记录--手写一个 v-tooltip 指令

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

前言

日常开发中,我们经常遇到过tooltip这种需求。文字溢出、产品文案、描述说明等等,每次都需要写一大串代码,那么有没有一种简单的方式呢,这回我们用指令来试试。

功能特性

  • 支持tooltip样式自定义
  • 支持tooltip内容自定义
  • 动态更新tooltip内容
  • 文字省略自动出提示
  • 支持弹窗位置自定义和偏移

功能实现

vue3中,指令也是拥有着对应的生命周期。

 我们这里需要使用的是 mountedupdatedunmounted钩子。

import { DirectiveBinding } from 'vue'
export default {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
  	
  },
  updated(el: HTMLElement, binding: DirectiveBinding) {
    
  },
  unmounted(el: HTMLElement) {

  }
}

在元素挂载完成之后,我们需要完成上述指令的功能。

什么时候可用?

首先我们需要考虑的是tooltip什么时候可用?

  • 元素是省略元素
  • 手动开启时,我们需要启用tooltip,比如描述或者产品文案等等。

如果是省略元素,我们需要先判断元素是否存在省略,一般通过这种方式判断:

function isOverflow(el: SpecialHTMLElement) {
  if (el.scrollWidth > el.offsetWidth || el.scrollHeight > el.clientHeight) {
    return true
  }
  return false
}
// element plus 采用如下方式判断,兼容 firefox
function isOverflow(el: SpecialHTMLElement){
  const range = document.createRange()
  range.setStart(el, 0)
  range.setEnd(el, el.childNodes.length)
  const rangeWidth = range.getBoundingClientRect().width
  const padding =
    (Number.parseInt(getComputedStyle(el)['paddingLeft'], 10) || 0) +
    (Number.parseInt(getComputedStyle(el)['paddingRight'], 10) || 0)
  if (
    rangeWidth + padding > el.offsetWidth ||
    el.scrollWidth > el.offsetWidth
  ) {
    return true
  }
  return false
}
我们也需要考虑手动开启这种情况,一般使用一个特殊的 CSS属性开启。
const enable = el.getAttribute('enableTooltip')
内容构造和位置计算

tooltip开启之后,我们需要构造它的内容和动态计算tooltip的位置,比如元素发生缩放和滚动。

构造tooltip内容的话,我们采用一个vue组件,然后通过动态组件方式,将其挂载为tooltip的内容。

<template>
  <div
    ref="tooltipRef"
    class="__CUSTOM_TOOLTIP_ITEM_CONTENT__"
    :class="arrow"
    @mouseover="mouseOver"
    @mouseleave="mouseLeave"
    v-html="content"
  ></div>
</template>

<script lang="ts" setup>
  import type { TimeoutHTMLElement } from './tooltip'
  defineProps({
    content: {
      type: String,
      default: '',
    },
    arrow: {
      type: String,
      default: '',
    },
  })
  const tooltipRef = ref()
  let parent: TimeoutHTMLElement
  onMounted(() => {
    parent = tooltipRef.value.parentElement
  })
  function mouseOver() {
    clearTimeout(parent.__hide_timeout__)
    parent.setAttribute('data-show', 'true')
    parent.style.visibility = 'visible'
  }
  function mouseLeave() {
    parent.setAttribute('data-show', 'false')
    parent.style.visibility = 'hidden'
  }
</script>
<style scoped lang="scss">
  $radius: 8px;
  @mixin arrow {
    position: absolute;
    border-style: solid;
    border-width: $radius;
    width: 0;
    height: 0;
    content: '';
  }

  .__CUSTOM_TOOLTIP_ITEM_CONTENT__ {
    position: absolute;
    border-radius: 4px;
    padding: 10px;
    width: 100%;
    max-width: 260px;
    font-size: 12px;
    color: #fff;
    background: rgb(45 46 50 / 80%);
    line-height: 18px;

    &.top::before {
      @include arrow;

      top: $radius * (-2);
      left: calc(50% - #{$radius});
      border-color: transparent transparent rgb(45 46 50 / 80%) transparent;
    }

    &.top-start::before .top-start::before {
      @include arrow;

      top: $radius * (-2);
      left: $radius;
      border-color: transparent transparent rgb(45 46 50 / 80%) transparent;
    }

    &.top-end::before &.top-end::before {
      @include arrow;

      top: $radius * (-2);
      left: calc(100% - #{$radius * 3});
      border-color: transparent transparent rgb(45 46 50 / 80%) transparent;
    }
  }
</style>
此外我们也可以通过 slot方式自定义提示内容。当然也可以通过属性查询 [slot='content']节点,取出其中的 innerHTML,但是这种在更新时需要特殊处理。
function parseSlot(vNode) {
  const content = vNode.children.find(i => {
    return i?.data?.slot === 'content'
  })
  const app = createApp({
    functional: true,
    props: {
      render: Function
    },
    render() {
      return this.render()
    }
  })
	const el = document.createElement('div')
  app.mount(el)
  return el?.innerHTML
}
tooltip位置计算和自动更新,这里我们使用 @floating-ui/dom库。
const __tooltip_el__ = document.createElement('div')
__tooltip_el__.className = '__CUSTOM_TOOLTIP__'
document.body.appendChild(__tooltip_el__)
function createEle() {
  const tooltip = document.createElement('div')
  tooltip.className = '__CUSTOM_TOOLTIP_ITEM__'
  tooltip.style['zIndex'] = '9999'
  tooltip.style['position'] = 'absolute'
  __tooltip_el__.appendChild(tooltip)
  return tooltip
}
function initTooltip(el: SpecialHTMLElement, binding: DirectiveBinding) {
  const tooltip = createEle()
  el.__float_tooltip__ = tooltip as unknown as TimeoutHTMLElement
  createTooltip(el, binding)
  autoUpdate(el, tooltip, () => updatePosition(el), {
    animationFrame: false,
    ancestorResize: false,
    elementResize: false,
    ancestorScroll: true,
  })
}
function createTooltip(el: SpecialHTMLElement, binding: DirectiveBinding) {
  const tooltip = el.__float_tooltip__ as HTMLElement
  const { width } = el.getBoundingClientRect()
  tooltip.style['minWidth'] = width + 'px'
  const arrow = el.getAttribute('arrow')
  // eslint-disable-next-line vue/one-component-per-file
  const app = createApp(tooltipVue, {
    arrow: arrow,
    content: binding.value !== void 0 ? binding.value : el.oldVNode,
  })
  app.mount(tooltip)
  el.__float_app__ = app
}
function updatePosition(el: SpecialHTMLElement) {
  const tooltip = el.__float_tooltip__
  const middlewares = []
  const visible = tooltip?.style?.visibility
  if (visible !== 'hidden' && visible) {
    const placement = el?.getAttribute('placement') || 'bottom'
    let offsetY =
      el?.getAttribute('offsetY') || el?.getAttribute('offset-y') || 5
    let offsetX = el?.getAttribute('offsetX') || el?.getAttribute('offset-x')
    const offsetXY = el?.getAttribute('offset')
    if (offsetXY !== null) {
      offsetX = offsetXY
      offsetY = offsetXY
    }
    if (offsetX || offsetY) {
      middlewares.push(
        offset({
          mainAxis: Number(offsetY),
          crossAxis: Number(offsetX),
        })
      )
    }

    computePosition(el, tooltip, {
      placement: placement as Placement,
      strategy: 'absolute',
      middleware: middlewares,
    }).then(({ x, y }) => {
      Object.assign(tooltip.style, {
        top: `${y}px`,
        left: `${x}px`,
      })
    })
  }
}
用户交互

在构造好tooltip之后,我们需要添加用户交互行为事件,比如用户移入目标元素,显示tooltip,移除目标元素,隐藏tooltip。这里我们加上hide-delay,即延迟隐藏,在设置offset时特别有用,同时也支持添加show-delay,延迟显示。

function attachEvent(el: HTMLElement) {
  el?.addEventListener?.('mouseover', mouseOver)
  el?.addEventListener?.('mouseleave', mouseLeave)
}

function mouseOver(evt: MouseEvent) {
  const el = evt.currentTarget as SpecialHTMLElement
  const tooltip = el?.__float_tooltip__
  clearTimeout(tooltip?.__hide_timeout__)
  if (tooltip) {
    tooltip.style.visibility = 'visible'
    tooltip.setAttribute('data-show', 'true')
    updatePosition(el)
  }
}

function mouseLeave(evt: MouseEvent) {
  const el = evt.currentTarget as SpecialHTMLElement
  const tooltip = el?.__float_tooltip__
  const isShow = tooltip?.getAttribute?.('data-show')
  const delay = el.getAttribute('hide-delay') || 100
  clearTimeout(tooltip?.__hide_timeout__)
  if (tooltip) {
    if (delay) {
      tooltip.__hide_timeout__ = setTimeout(() => {
        if (isShow === 'true') {
          tooltip.style.visibility = 'hidden'
        }
      }, +delay)
    } else {
      if (isShow === 'true') {
        tooltip.style.visibility = 'hidden'
      }
    }
  }
}
内容更新

我们tooltip的内容并不总是一成不变的,所以我们需要支持内容更新,这个可以在updated钩子中完成内容更新。

既然我们支持了指令传值和slot方式,所以我们需要考虑三点:

  • 指令值变化
  • slot内容变化
  • 开启和关闭

对于slot内容变化监测,我们可以对比新旧slot内容,内容不同则触发更新。

{
  updated(el: SpecialHTMLElement, binding: DirectiveBinding, vNode: VNode) {
    if (binding.value !== binding.oldValue) {
      updated(el, binding)
    } else {
      const enable = el.getAttribute('enableTooltip')
      if (enable !== el.oldEnable) {
        mounted(el, binding, vNode)
      } else {
        const newVNode = parseSlot(vNode)
        if (el.oldVNode !== newVNode) {
          el.oldVNode = newVNode
          updated(el, binding)
        }
      }
    }
  },
}
function updated(el: SpecialHTMLElement, binding: DirectiveBinding) {
  el?.__float_app__?.unmount?.()
  el.__float_app__ = null
  createTooltip(el, binding)
}
销毁tooltip

最后,在元素销毁或者tooltip关闭的的时候,我们需要把相应的事件等进行销毁。

function unmounted(el: SpecialHTMLElement) {
  removeEvent(el)
  const tooltip = el?.__float_tooltip__
  if (tooltip) {
    __tooltip_el__.removeChild(tooltip)
    el?.__float_app__?.unmount?.()
    el.__float_app__ = null
    el.__float_tooltip__ = null
  }
}

function removeEvent(el: HTMLElement) {
  el?.removeEventListener?.('mouseover', mouseOver)
  el?.removeEventListener?.('mouseleave', mouseLeave)
}
完整代码
import { DirectiveBinding, VNode, App } from 'vue'
import {
  computePosition,
  autoUpdate,
  offset,
  Placement,
} from '@floating-ui/dom'

import tooltipVue from './CustomTooltip.vue'

export type TimeoutHTMLElement = HTMLElement & {
  __hide_timeout__: NodeJS.Timeout
}
export type SpecialHTMLElement =
  | HTMLElement & {
      __float_tooltip__: TimeoutHTMLElement | null
    } & {
      __float_app__: App | null
    } & {
      oldEnable: string | null
    } & {
      oldVNode: string
    }

// tooltip 容器
const __tooltip_el__ = document.createElement('div')
__tooltip_el__.className = '__CUSTOM_TOOLTIP__'
document.body.appendChild(__tooltip_el__)
// 判断是否溢出
function isOverflow(el: SpecialHTMLElement) {
  if (el.scrollWidth > el.offsetWidth || el.scrollHeight > el.clientHeight) {
    return true
  }
  return false
}
// 清除 slot
function emptySlot(el: SpecialHTMLElement) {
  const slot = el.querySelector("[slot='content']")
  if (slot) {
    el.removeChild(slot)
  }
  return slot?.innerHTML
}
// 卸载
function unmounted(el: SpecialHTMLElement) {
  removeEvent(el)
  const tooltip = el?.__float_tooltip__
  if (tooltip) {
    __tooltip_el__.removeChild(tooltip)
    el?.__float_app__?.unmount?.()
    el.__float_app__ = null
    el.__float_tooltip__ = null
  }
}
// 移除事件
function removeEvent(el: SpecialHTMLElement) {
  el?.removeEventListener?.('mouseover', mouseOver)
  el?.removeEventListener?.('mouseleave', mouseLeave)
}
// 添加事件
function attachEvent(el: SpecialHTMLElement) {
  el?.addEventListener?.('mouseover', mouseOver)
  el?.addEventListener?.('mouseleave', mouseLeave)
}
// 鼠标悬浮
function mouseOver(evt: MouseEvent) {
  const el = evt.currentTarget as SpecialHTMLElement
  const tooltip = el?.__float_tooltip__
  clearTimeout(tooltip?.__hide_timeout__)
  if (tooltip) {
    tooltip.style.visibility = 'visible'
    tooltip.setAttribute('data-show', 'true')
    updatePosition(el)
  }
}
// 鼠标移出
function mouseLeave(evt: MouseEvent) {
  const el = evt.currentTarget as SpecialHTMLElement
  const tooltip = el?.__float_tooltip__
  const isShow = tooltip?.getAttribute?.('data-show')
  const delay = el.getAttribute('hide-delay') || 100
  clearTimeout(tooltip?.__hide_timeout__)
  if (tooltip) {
    if (delay) {
      tooltip.__hide_timeout__ = setTimeout(() => {
        if (isShow === 'true') {
          tooltip.style.visibility = 'hidden'
        }
      }, +delay)
    } else {
      if (isShow === 'true') {
        tooltip.style.visibility = 'hidden'
      }
    }
  }
}
// 挂载tooltip
function mounted(
  el: SpecialHTMLElement,
  binding: DirectiveBinding,
  vNode: VNode
) {
  const overflow = isOverflow(el)
	// 手动启用tooltip
  const enable = el.getAttribute('enableTooltip')
  el.oldEnable = enable
  if (binding.value === void 0 && vNode) {
    el.oldVNode = parseSlot(vNode)
  }
  emptySlot(el)
  // 显示延迟
  const delay = el.getAttribute('show-delay') || 100
  if (overflow || enable === 'true') {
    if (delay) {
      setTimeout(() => {
        initTooltip(el, binding)
        attachEvent(el)
      }, +delay)
    } else {
      initTooltip(el, binding)
      attachEvent(el)
    }
  } else {
    unmounted(el)
  }
}
// 更新tooltip 只更新内容
function updated(el: SpecialHTMLElement, binding: DirectiveBinding) {
  el?.__float_app__?.unmount?.()
  el.__float_app__ = null
  createTooltip(el, binding)
}
// 创建元素工厂
function createEle() {
  const tooltip = document.createElement('div')
  tooltip.className = '__CUSTOM_TOOLTIP_ITEM__'
  tooltip.style['zIndex'] = '9999'
  tooltip.style['position'] = 'absolute'
  __tooltip_el__.appendChild(tooltip)
  return tooltip
}
// 初始化tooltip:创建和计算位置
function initTooltip(el: SpecialHTMLElement, binding: DirectiveBinding) {
  const tooltip = createEle()
  el.__float_tooltip__ = tooltip as unknown as TimeoutHTMLElement
  createTooltip(el, binding)
  autoUpdate(el, tooltip, () => updatePosition(el), {
    animationFrame: false,
    ancestorResize: false,
    elementResize: false,
    ancestorScroll: true,
  })
}
// 创建tooltip
function createTooltip(el: SpecialHTMLElement, binding: DirectiveBinding) {
  const tooltip = el.__float_tooltip__ as HTMLElement
  const { width } = el.getBoundingClientRect()
  tooltip.style['minWidth'] = width + 'px'
  const arrow = el.getAttribute('arrow')
  // eslint-disable-next-line vue/one-component-per-file
  const app = createApp(tooltipVue, {
    arrow: arrow,
    content: binding.value !== void 0 ? binding.value : el.oldVNode,
  })
  app.mount(tooltip)
  el.__float_app__ = app
}
// 更新tooltip位置
function updatePosition(el: SpecialHTMLElement) {
  const tooltip = el.__float_tooltip__
  const middlewares = []
  const visible = tooltip?.style?.visibility
  if (visible !== 'hidden' && visible) {
    const placement = el?.getAttribute('placement') || 'bottom'
    let offsetY =
      el?.getAttribute('offsetY') || el?.getAttribute('offset-y') || 5
    let offsetX = el?.getAttribute('offsetX') || el?.getAttribute('offset-x')
    const offsetXY = el?.getAttribute('offset')
    if (offsetXY !== null) {
      offsetX = offsetXY
      offsetY = offsetXY
    }
    if (offsetX || offsetY) {
      middlewares.push(
        offset({
          mainAxis: Number(offsetY),
          crossAxis: Number(offsetX),
        })
      )
    }

    computePosition(el, tooltip, {
      placement: placement as Placement,
      strategy: 'absolute',
      middleware: middlewares,
    }).then(({ x, y }) => {
      Object.assign(tooltip.style, {
        top: `${y}px`,
        left: `${x}px`,
      })
    })
  }
}
// 解析slot
function parseSlot(vNode: VNode) {
  const content = (vNode.children as VNode[]).find?.((i: VNode) => {
    return i?.props?.slot === 'content'
  })
  // eslint-disable-next-line vue/one-component-per-file
  const app = createApp(
    {
      functional: true,
      props: {
        render: Function,
      },
      render() {
        return this.render()
      },
    },
    // eslint-disable-next-line vue/one-component-per-file
    {
      render: () => {
        return content
      },
    }
  )
  const el = document.createElement('div')
  app.mount(el)
  return el?.innerHTML
}

export default {
  mounted(el: SpecialHTMLElement, binding: DirectiveBinding, vNode: VNode) {
    mounted(el, binding, vNode)
  },
  updated(el: SpecialHTMLElement, binding: DirectiveBinding, vNode: VNode) {
    if (binding.value !== binding.oldValue) {
      updated(el, binding)
    } else {
      const enable = el.getAttribute('enableTooltip')
      if (enable !== el.oldEnable) {
        mounted(el, binding, vNode)
      } else {
        const newVNode = parseSlot(vNode)
        if (el.oldVNode !== newVNode) {
          el.oldVNode = newVNode
          updated(el, binding)
        }
      }
    }
  },
  unmounted(el: SpecialHTMLElement) {
    unmounted(el)
  },
}
示例
<div v-tooltip='hello world' enableTooltip='true'>tooltip</div>

<div v-tooltip enableTooltip='true'>
  tooltip
	<div slot='content'>
    <div>this is a tooltip</div>
    <button>confirm</button>
  </div>
</div>

总结

在经过二次封装之后,我们只需要v-tooltip这样简便的操作,即可达到tooltip的作用,简化了传统的书写流程,对于一些特殊tooltip内容,我们可以通过slot方式,定制化更多的提示内容。

本文转载于:

https://juejin.cn/post/7177384845968932901

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第一百七十六回 如何创建渐变色边角

文章目录 1. 概念介绍2. 实现方法3. 代码与细节3.1 示例代码3.2 代码细节 4. 内容总结 我们在上一章回中介绍了"如何创建放射形状渐变背景"相关的内容&#xff0c;本章回中将介绍"如何创建渐变色边角".闲话休提&#xff0c;让我们一起Talk Flutter吧。 1.…

Axios使用方式

ajax是JQUERY封装的XMLHttprequest用来发送http请求 Axios简单点说它就是一个js库,支持ajax请求,发送axios请求功能更加丰富,丰富在哪不知道 1.npm使用方式 vue项目中 npm install axios 2.cdn方式 <script src"https://unpkg.com/axios/dist/axios.min.js">…

行情分析——加密货币市场大盘走势(11.22)

大饼昨日晚上打了止损&#xff0c;笔者入场了空单&#xff0c;目前来看上涨乏力&#xff0c;下跌是必然的&#xff0c;昨日的下跌跌破了蓝色上涨趋势线&#xff0c;而今日白天开始反弹&#xff0c;别着急抄底&#xff0c;下跌还没有结束。 空单策略&#xff1a;入场36500 止盈…

为UE和Unity开发者准备的Godot指南

为UE和Unity开发者准备的Godot指南 ——两位大哥打架&#xff0c;请带上我 这两天游戏行业又开始热闹了&#xff0c;昨天两条信息直接刷爆朋友圈&#xff0c;最大的两家游戏引擎公司怼起来了。 《为Unity开发者准备的虚幻引擎指南》&#xff1a; 为Unity开发者准备的虚幻引擎指…

Autocad2020切换经典界面

Autocad2020切换经典界面 1.更改1.1设置另存为 1.更改 1.1设置另存为

SQL语句执行过程

一条 SQL 的执行过程可以大致分为以下几个步骤&#xff1a; 连接器&#xff1a; ○ 客户端与数据库建立连接&#xff0c;并发送 SQL 语句给数据库服务。 ○ 连接器验证客户端的身份和权限&#xff0c;确保用户有足够的权限执行该 SQL 语句。查询缓存&#xff1a; ○ 连接器首先…

Redis面试内容,Redis过期策略,Redis持久化方式,缓存穿透、缓存击穿和缓存雪崩,以及解决办法

文章目录 一、redis什么是RedisRedis使用场景1、缓存2、数据共享[分布式](https://so.csdn.net/so/search?q分布式&spm1001.2101.3001.7020)3、分布式锁4、全局ID5、计数器6、限流7、位统计 Redis有5中数据类型&#xff1a; SSHLZRedis中一个key的值每天12点过期&#xff…

编码的发展历史

编码的发展历史 ASCII&#xff1a; ASCII编码使用7位二进制数表示一个字符&#xff0c;范围从0到127。每个字符都有一个唯一的ASCII码值与之对应。例如&#xff0c;大写字母"A"的ASCII码是65&#xff0c;小写字母"a"的ASCII码是97。 ASCII字符集包括英文…

【python基础】python可变序列与不可变序列

文章目录 前言一、序列类型定义二、对序列类型的切片操作三、使用 与 * 对序进行操作四、增量赋值 和 * 前言 本文主要讲可变序列与不可变序列一些简单的应用。 一、序列类型定义 按序列能否被修改分为&#xff1a;可变序列与不可变序列。 可变序列&#xff1a;可以进行增、…

短期风速预测|LSTM|ELM|批处理(matlab代码)

1主要内容 该程序是预测类的基础性代码&#xff0c;程序对河北某地区的气象数据进行详细统计&#xff0c;程序最终得到pm2.5的预测结果&#xff0c;通过更改数据很容易得到风速预测结果。程序主要分为三部分&#xff0c;分别是基于LSTM算法、基于ELM算法和基于LSTM和批处理组合…

vivado产生报告阅读分析15-时序报告11

Report Clock Domain Crossings “ Clock Domain Crossings (CDC) ” &#xff08; 时钟域交汇 &#xff09; 报告可对设计中的时钟域交汇执行结构分析。此信息可用于识别潜在不安全的 CDC &#xff0c; 此类 CDC 可能导致亚稳态或数据一致性问题。虽然 CDC 报告与“ Clock …

2023年危险化学品经营单位主要负责人证模拟考试题库及危险化学品经营单位主要负责人理论考试试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年危险化学品经营单位主要负责人证模拟考试题库及危险化学品经营单位主要负责人理论考试试题是由安全生产模拟考试一点通提供&#xff0c;危险化学品经营单位主要负责人证模拟考试题库是根据危险化学品经营单位主…

LongAccumulator

原子操作之LongAccumulator 和LongAdder的区别在于&#xff0c;LongAdder是在Cell里面只能做加减操作&#xff0c;不能乘除&#xff0c;而LongAccumulator就可以定义乘除操作。原理和LongAdder都是一样的&#xff0c;一个Base和一个Cells数组。 原文跳转地址

基于docker实现JMeter分布式压测

为什么需要分布式&#xff1f; 在工作中经常需要对一些关键接口做高QPS的压测&#xff0c;JMeter是由Java 语言开发&#xff0c;没创建一个线程&#xff08;虚拟用户&#xff09;&#xff0c;JVM默认会为每个线程分配1M的堆栈内存空间。受限于单台试压机的配置很难实现太高的并…

prometheus热更新失败failed to reload config

一、问题描述 k8s部署的prometheus服务在请求热更新时报错: failed to reload config: one or more errors occurred while applying the new configuration (--config.file"/etc/prom/config/file/prometheus.yml")请求命令:curl -X POST http://monitor-cp-prom:…

【Delphi】开发IOS 程序,TLabel 中英文字对齐(水平),一行代码解决显示对齐问题!

目录 一、问题现象&#xff1a; 二、解决方案&#xff08;一行代码解决ios对齐问题&#xff09;&#xff1a; 三、解决后效果&#xff1a; 四、后记&#xff1a; 一、问题现象&#xff1a; 在用 Delphi 开发ios程序时&#xff0c;使用TLabel控件显示&#xff0c;会出现中英…

Epub书籍阅读工具

Epub书籍阅读工具 前言WIndows总结Neat ReaderAquile ReaderWPS Android总结Neat Reader掌阅 前言 Epub文件为电子书文件格式&#xff0c;此格式的电子书相比txt书籍&#xff0c;增加了目录跳转功能&#xff0c;并可以显示图片。本文介绍WIndows和Android端的epub书籍阅读工具…

如何做好项目管理?年薪百万项目大佬一直在用这11张图

大家好&#xff0c;我是老原。 日常工作中&#xff0c;我们会遇到各种大大小小的工作项目&#xff0c;如何能让项目保质保量的完成&#xff0c;是我们项目经理的目标。 项目管理的流程可以说是由一系列的子过程组成的&#xff0c;它是一个循序渐进的过程&#xff0c;所以不能…

ros2不同机器通讯时IP设置

看到这就是不同机器的IP地址&#xff0c;为了避免在路由器为不同的机器使用DHCP分配到上面的地址&#xff0c;可以设置DHCP分配的范围&#xff1a;&#xff08;我的路由器是如下设置的&#xff0c;一般路由器型号都不一样&#xff0c;自己找一下&#xff09; 防火墙设置-----&…

langchain 部署组件-LangServe

原文&#xff1a;&#x1f99c;️&#x1f3d3; LangServe | &#x1f99c;️&#x1f517; Langchain LangServe &#x1f6a9; We will be releasing a hosted version of LangServe for one-click deployments of LangChain applications. Sign up here to get on the wa…