前端开发小技巧积累

93 阅读25分钟

CSS

实现卡片多行排列布局

使用align-content: space-between结合nth-child来实现

假设如果永远是第3n的元素是最后一列

<template>
  <div class="container">
    <div class="crad">1</div>
    <div class="crad">2</div>
    <div class="crad">3</div>
    <div class="crad">4</div>
    <div class="crad">5</div>
    <div class="crad">6</div>
  </div>
</template>
<style lang="less" scoped>
.container{
  display: flex;
  width:630px;
  align-content: space-between;
  flex-flow: wrap;
  .crad{
    height:100px;
    background: blueviolet;
    width:200px;
    margin-bottom: 16px;
    margin-right: 16px;
    &:nth-child(3n) {
      margin-right: 0;
    }
  }
</style>
}

依然使用align-content: space-between结合nth-child来实现,但是容器宽度可变化,每行卡片数量也可变化

<template>
  <div class="card-content-box">
    <div class="container">
        <div class="crad">1</div>
        <div class="crad">2</div>
        <div class="crad">3</div>
        <div class="crad">4</div>
        <div class="crad">5</div>
   </div>
  </div>
</template>
<style lang="less" scoped>
.card-content-box{
    width:100%;
    background:red;
    overflow: hidden;
    .container{
      width: calc(100% + 16px); // 加上每个card的margin-rightdisplay: flex;
      align-content: space-between;
      flex-flow: wrap;
      .crad{
        height:100px;
        background: blueviolet;
        width:200px;
        margin-right: 16px;
      }
    }
}
</style>

使用align-content: space-between结合gap来实现(推荐)

<template>
  <div class="container">
        <div class="crad">1</div>
        <div class="crad">2</div>
        <div class="crad">3</div>
        <div class="crad">4</div>
        <div class="crad">5</div>
   </div>
</template>
<style lang="less" scoped>
.container{
  width: 100%;
  display: flex;
  align-content: space-between;
  flex-flow: wrap;
  gap: 16px
  .crad{
    height:100px;
    background: blueviolet;
    width:200px;
  }
}
</style>

使用grid来实现(推荐)

<template>
  <div class="container">
        <div class="crad">1</div>
        <div class="crad">2</div>
        <div class="crad">3</div>
        <div class="crad">4</div>
        <div class="crad">5</div>
   </div>
</template>
<style lang="less" scoped>
.container{
  width: 100%;
  display: grid;
  grid-template-columns: repeat(auto-fill, 200px);
  gap: 16px
  .crad{
    height:100px;
    background: blueviolet;
    width:200px;
  }
}
</style>

grid-template-columns: repeat()

grid-template-columns: repeat() 是CSS Grid布局中的一个属性值,用于定义网格容器中水平方向(列)的网格轨道(track)结构。具体语法如下:

grid-template-columns: repeat(number, size1, size2, ...);
  • number: 表示每行中重复的次数。例如,如果要创建一行3次循环排布,则填入数字 3。
  • [size]...: 可以是具体的长度值(如像素、百分比、fr 等)、auto 或者是一个 Track-Breadth(网格轨道大小)。这个参数可以有多个,每个值对应一个循环中一列的大小。如果为一个值,则该循环所有列将使用相同的默认大小。

auto-fill

自动填充,让一行或一列中尽可能容纳更多的单元格。

fr

网格容器中可用空间的几等份。拿到剩余部分,看当前网格容器写了几个fr,就表示均为多少分,根据fr的数值进行划分。

/* fr - 关键字 - 单位 */
.grid-container {
  /* 声明了3列 */
  /* 第一列宽度:200px */
  /* 第二列宽度:将网格容器剩余空间均分为3等份,第二列占一份 */
  /* 第三列宽度:将网格容器剩余空间均分为3等份,第三列占两份 */
  grid-template-columns: 200px 1fr 2fr;
  grid-auto-rows: 100px;
}

minmax() 函数

minmax()函数产生一个长度范围,表示长度在这个范围之中都可以应用到网格项目中。

  • 它接收两个参数:

    • 第一个参数:最小值;
    • 第二个参数:最大值;
/* minmax() - 最大最小范围函数 */
.grid-container {
  grid-template-columns: 1fr 1fr minmax(200px, 2fr);
  grid-auto-rows: 100px;
}

auto关键字

通过 auto关键字,我们可以轻松实现两列甚至更多列的布局(其中的一列或某几列宽度固定不变,其他的列宽度随着屏幕的宽度自适应)。

/* auto关键字  */
.grid-container {
  /* 声明了三列 */
  /* 第一列 和 第三列 列宽维 100px */
  /* 第二列的宽度由浏览器的可视化窗口的宽度决定 */
  grid-template-columns: 100px auto 100px;
  grid-auto-rows: 100px;
}

设置 行间距 和 列间距(gap + row-gap + column-gap)

属性作用
gap设置 行间距 和 列间距(先行后列
row-gap设置 行间距
column-gap设置 列间距

行间距 和 列间距

/* row-gap: 30px; */
/* column-gap: 60px; */
/* 上面的代码等同于下面这行代码 */
gap: 30px 60px; /* 行间距, 列间距 */

元素排列方式(grid-auto-flow)

属性名属性值描述
grid-auto-flowrow默认值,按  开始排列,即先填满一行,再开始下一行
grid-auto-flowcolumn按  开始排列
grid-auto-flowdense该关键字指定自动布局算法使用一种"稠密"堆积算法,如果后面出现了稍小的元素,则会试图去填充网格中前面留下的空白。这样做会填上稍大元素留下的空白,但同时也可能导致原来出现的次序被打乱
grid-auto-flowrow dense按行来填充网格中前面留下的空白
grid-auto-flowcolumn dense按列来填充网格中前面留下的空白

设置网格在容器中的位置(justify-content + align-content)

  • justify-content

    • 该属性用于对齐容器内的网格,设置如何分配顺着弹性容器主轴(或者网格行轴)的元素之间及其周围的空间;
    • 简单来说就是根据 网格行轴 设置 网格位置 以及 列间距
  • align-content

    • 该属性用于设置 垂直方向 上的 网格元素 在容器中的 对齐方式
    • 简单来说就是根据 网格列轴 设置 网格位置 及其 行间距
  • ❗❗ 注意

    • 网格的总宽度 必须小于 容器的宽度 才能使 justify-content 属性生效;
    • 网格元素的总高度 必须小于 容器的高度 才能使 align-content 属性生效;
    • justify-contentalign-content 的属性值是一样的;

justify-content + align-content

 设置网格内容在网格中的位置(justify-items + align-items)

justify-items + align-items

定位项目位置(grid-area)

  • 语法:

    • grid-area: grid-row-start / grid-column-start / grid-row-end / grid-column-end | itemname;
描述
grid-row-start指定在 哪一行 开始显示 网格元素
grid-column-start指定在 哪一列 开始显示 网格元素
grid-row-end指定 哪一行 停止显示 网格元素
grid-column-end指定 哪一列 停止显示 网格元素

justify-self、align-self、place-self

  • justify-self 属性设置单元格内容的水平位置(左中右),与 justify-items属性的用法完全一致,但只作用于单个项目;
  • align-self 属性设置单元格内容的垂直位置(上中下),与 align-items属性的用法完全一致,但只作用于单个项目;
  • place-self属性 是 上述两个属性的复合属性 先 justify-selfalign-self
  • 上述属性的属性值是一样的:start(对齐单元格的起始边缘) | center(居中) | end(对齐单元格的结束边缘) |stretch(默认值,拉伸,占满整个单元格)

横竖屏样式变更

<style>
@media all and (orientation : landscape) {
    body {
        background-color: #ff0000;
    }
}

@media all and (orientation : portrait) {
    body {
        background-color: #00ff00;
    }
}
</style>

背景动画效果

利用CSS的过渡和动画属性,创建平滑的过渡、淡入淡出效果。通过定义动画的持续时间、延迟时间和重复次数,可以控制动画的表现方式。

.box {
  transition: background-color 0.3s ease-in-out;
}

.box:hover {
  background-color: #ff5500;
}

平滑滚动效果

通过使用CSS的scroll-behavior属性,可以添加平滑滚动效果,使页面在滚动时更加流畅和舒适。

html {
  scroll-behavior: smooth;
}

渐变边框样式

使用CSS的border-image属性,你可以创建具有渐变效果的边框样式。定义渐变图像或渐变颜色作为边框的源,以及边框的切片方式和宽度。

.border {
  border: 10px solid;
  border-image: linear-gradient(to right, #ff5500, #ffd200) 1;
}

渐变文本效果

使用CSS的background-clip属性和渐变背景色,可以为文本创建渐变效果。将渐变应用到文本的背景区域,形成独特的渐变文本效果。

.text {
  background: linear-gradient(to right, #ff5500, #ffd200);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

:is伪类选择器

通常需要将相同的样式应用于多个元素。:is()不能匹配::before::after伪元素。

/* default black */
p {
  color: #000;
}

/* gray in <article>, <section>, or <aside> */
article p,
section p,
aside p {
  color: #444;
}

:is(article, section, aside) p {
  color: #444;
}

:where伪类选择器

:where()选择器语法与:is()相同,并且在所有现代浏览器(不包括IE)中都支持。这通常会导致相同的样式。区别在于特异性,在:where()的情况下,特异性为零。基于:where()的零特异性,可以用于初始样式设置。

/* reset */
:where(h2) {
  margin-block-start: 1em;
}

:where(article :first-child) {
  margin-block-start: 0;
}

:has伪类选择器(兼容性差不建议使用)

/* 选择 h2 中间第一行 */
h2 + :has(~ h2){
  color:green;
}
/* 选择 h2 中间最后一行 */
h2 ~ :has(+ h2){
  color:red;
}

h2 + :has(~ h2) 表示选择紧跟着 h2 的并且后面还有 h2 元素的兄弟元素。也就选择到了 h2 范围内的第一个元素。

h2 ~ :has(+ h2) 表示选择 h2 后面的兄弟元素,并且该兄弟元素的下一个兄弟元素是 h2,也就选择到了 h2 范围内最后一个元素

transform-style: preserve-3d实现3d效果

  • transform-style: preserve-3d; 表示所有子元素在3D空间中呈现

  • perspective: 400; 3D元素的透视效果

  • transition: transform 1.5s linear; 过渡效果

  • backface-visibility: hidden; 定义当元素背面向屏幕时不可见。

  • transform: rotateY(180deg); 沿Y轴旋转180度

<div class="film">
  <!-- 正面 -->
  <div class="face">
    <img src="https://i.postimg.cc/RCQsrfyN/lyf1.jpg" alt="">
  </div>
  <!-- 反面 -->
  <div class="face back">
    <div class="content">
      刘亦菲,1987年8月25日出生于湖北省武汉市,华语影视女演员、歌手,毕业于北京电影学院2002级表演系本科。2002年因出演电视剧《金粉世家》中白秀珠一角踏入演艺圈。2003年因出演武侠剧《天龙八部》中王语嫣一角崭露头角。2004年凭借仙侠剧《仙剑奇侠传》赵灵儿一角获得了高人气与关注度。
    </div>
  </div>
</div>

.film {
  position: relative;
  width: 220px;
  height: 320px;
  margin: 200px auto 0;
  transform-style: preserve-3d; /*指定所有子元素在3D空间内呈现*/
  perspective: 400; /*元素距离视图的距离*/
  transition:  transform 1.5s linear; /*设置过渡效果*/
}
.face {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  backface-visibility: hidden; /*定义当元素背面向屏幕时不可见*/
}
img {
  width: 100%;
  height: 100%;
}
.back {
  background: linear-gradient(to bottom, rgb(0, 0, 0), rgb(17, 17, 16));
  transform: rotateY(180deg);/*旋转180度,转向背面*/
}
.film:hover {
  cursor: pointer;
  transform: rotateY(180deg) scale(1.5); /*鼠标悬停时,旋转180度,将背面展示出来*/
}

.content {
  padding: 10px 20px;
  color: #fff;
  box-sizing: border-box;
}

nth-child选中指定的元素

// 前三个
li:nth-child(-n + 3) {
  text-decoration: underline;
}

// 选中 2-5 的列表项
li:nth-child(n + 2):nth-child(-n + 5) {
  color: #2563eb;
}

// 倒数两个
li:nth-last-child(-n + 2) {
  text-decoration-line: line-through;
}

省略号

/*单行*/
.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/*多行*/
.truncate {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

设置文字撑满盒子

text-align-last:justify;
word-break: keep-all;

JS

复制

let copy_text = document.querySelector(".draw-geojson-data").innerHTML; //拿到想要复制的值   
navigator.clipboard.writeText(copy_text); 
alert("复制成功!");//弹出提示信息,不同组件可能存在写法不同

// 低版本浏览器不支持navigator.clipboard.writeText 考虑到兼容性 建议使用下面方法
const input = document.createElement('input')
input.style.cssText = 'opacity: 0;'
input.type = 'text'
input.value = copy_text
document.body.appendChild(input)
input.select() // 选中文本
document.execCommand('copy') // 执行浏览器复制命令
document.body.removeChild(input)
alert("复制成功!");//弹出提示信息,不同组件可能存在写法不同

检查对象是空对象

const isEmptyObject = Object.keys(object).length === 0 && object.constructor === Object;

判断数字

function isNumber(value) {
  return typeof value === 'number';
}

判读数字是否为int2 int4 int8 float4 float8

function isint2(num) {
    return typeof num === 'number' && Number.isInteger(num) && num >= -32768 && num <= 32767
}

function isint4(num) {
    return typeof num === 'number' && Number.isInteger(num) && num >= -2147483648 && num <= 2147483647
}

function isint8(num) {
    return (
      typeof num === 'number' && Number.isInteger(num) && num >= -9223372036854775808 && num <= 9223372036854775807
    )
}
// js判断 float4
function isfloat4(num) {
    // 整数也认为是浮点型
    if (Number.isFinite(num) && typeof num === 'number' && num.indexOf('.') === -1) return true
    // 检查数字是否是NaN或无穷大
    if (isNaN(num) || !isFinite(num)) {
      return false
    }

    // 将数字转换为二进制字符串
    let binaryStr = num.toString(2)

    // 检查小数点后是否有足够的位数
    let dotIndex = binaryStr.indexOf('.')
    /* if (dotIndex === -1) {
      return false // 没有小数点,不是float
    } else */ if (
      dotIndex + 23 <
      binaryStr.length
    ) {
      return false // 小数点后有23位以上,是double
    } else {
      // 尝试通过移除尾随零来增加精度
      let precision = binaryStr.length - dotIndex - 1
      let exponent = 23 + dotIndex
      let mantissa = parseInt(binaryStr, 2)

      // 通过指数和尾数重建浮点数
      let reconstructed = ((Math.pow(2, exponent - 127) - 1) * mantissa) / Math.pow(2, 23)

      // 如果重建的数字与原始数字非常接近,则认为它可能是float
      return Math.abs(num - reconstructed) < Math.pow(2, -52)
    }
}
function isfloat8(num) {
    return Number.isFinite(num) && typeof num === 'number'
}

判断字符串是否为数字

// 判断是否为数字 包括字符串
function isNumeric(str) {
    return Number.isFinite(+str)
}

日期格式化

const formattedDate = new Date().toISOString().slice(0, 10);

将字符串转为整数类型

const intValue = +str;

打印dom元素

当你需要在开发过程中打印dom元素时,使用console.log往往只会打印出整个dom元素,而无法查看该dom元素的内部属性。你可以尝试使用console.dir

console.dir(document.body);

元素可编辑

当你需要在某个dom元素进行编辑,让它点击的时候就像一个textarea

<div contenteditable="true">这里是可编辑的内容</div>

递归函数名解耦

当你需要写一个递归函数的时候,你声明了一个函数名,但是每次修改函数名的时候总会忘记修改内部的函数名。argument为函数内部对象,包含传入函数的所有参数,arguments.callee代表函数名。

// 这是一个最基础的斐波那契数列
function fibonacci (n) {
    const fn = arguments.callee
    if (n <= 1) return 1
    return fn(n - 1) + fn(n - 2)
}

?. 和 ?? 和 !.

?.称为链判断运算,它允许开发人员读取深度嵌套在对象链中的属性值,而无需验证每个引用,当引用为空时,表达式停止计算并返回 undefined,而??则比较类似于||,但是专门作用于undefinednull

data?.layer1?.layer2?.[1]
undefined??true // true

!.是TypeScript的语法,叫非空断言操作符(non-null assertion operator),和?.相反,这个符号表示对象后面的属性一定不是null或undefined。

!.: 非空类型断言 表示确定某个标识符是有值的,跳过ts在编译阶段对它的检测 
function aa(value?:string) { 
    console.log(value!.length); 
    // console.log(value.length); //错误提醒: value is possibly 'undefined'. 
} 
aa('ppp'); 
//实际上这个东东不好用, 因为不传值, 编译JS后, 会报错, 所以建议使用?. 替代 !.

生成随机颜色

function getRandomColor() {  
    const colorAngle = Math.floor(Math.random() * 360);  
    return `hsla(${colorAngle},100%,50%,1)`;
}

Promise.allSettled

Promise.allSettled() 方法接受一组 Promise 实例作为参数,包装到一个新的 Promise 实例中。在所有这些参数实例都返回结果(已完成或已拒绝)之前,包装器实例不会结束。

有时候,我们并不关心异步请求的结果,我们只关心是否所有请求都结束了。这时候,Promise.allSettled() 方法就非常有用了。

const promises = [fetch("index.html"), fetch("https://does-not-exist/")];

const results = await Promise.allSettled(promises);

// Filter out successful requests
const successfulPromises = results.filter((p) => p.status === "fulfilled");

// Filter out failed requests and output the reason
const errors = results.filter((p) => p.status === "rejected").map((p) => p.reason);

获取列表中的最后一项

let array = [0, 1, 2, 3, 4, 5, 6, 7];

console.log(array.slice(-1)) >>> [7];
console.log(array.slice(-2)) >>> [6, 7];
console.log(array.slice(-3)) >>> [5, 6, 7];

slice

slice(start, end)
start: 开始位置的索引(从0开始)
end: 结束位置的索引(但该位置的元素本身不包括在内) 而不是截取的字符串或者数组长度

indexOf的按位化简

// bad
if (arr.indexOf(item) > -1) {  
    // item found
}
if (arr.indexOf(item) === -1) {  
    // item not found
}

// better
if (~arr.indexOf(item)) {  
    // item found
}
if (!~arr.indexOf(item)) {  
// item not found
}

//The bitwise (~) operator will return true (except for -1), //the reverse operation only requires !~. Alternatively, the includes() function can be used.
if (arr.includes(item)) {  
    // true if the item found
}

计算字符串某字符的数量

function countCharacter(str, charToCount) {

    // 创建一个正则表达式,它将匹配我们想要计数的字符

    const regex = new RegExp(charToCount, 'g');

    // 使用正则表达式的match方法来获取匹配到的所有字符,然后返回数组的长度

    return (str.match(regex) || []).length;

}

经纬度的范围校验

// 经度校验
function isValidLongitude(longitude) {
   return /^(\-|\+)?(((\d|[1-9]\d|1[0-7]\d|0{1,3})\.\d{0,15})|(\d|[1-9]\d|1[0-7]\d|0{1,3})|180\.0{0,15}|180)$/.test(
          longitude
        )
}

// 纬度校验
function isValidLatitude(latitude) {
    return /^(\-|\+)?([0-8]?\d{1}\.\d{0,15}|90\.0{0,15}|[0-8]?\d{1}|90)$/.test(latitude)
}

js 数组删除指定数据

使用filter()函数

arr = arr.filter(item => item !== target);

使用splice()函数

let index = arr.indexOf(target);

if (index > -1) {
    arr.splice(index, 1);
}

正则-判断字符串中是否包含特殊字符以及空格

var pattern = new RegExp("[`~!@#$^&*()=|{}':;',\\[\\].<>《》/?~!@#¥……&*()——|{}【】‘;:”“'。,、? ]")
if (pattern.test(value)) {
    callback('表名不允许有特殊字符以及空格!')
}

正则-以字母开头的包含字母或下划线的字符串

最后一位可为下划线

new RegExp(/^(?!_)[A-Za-z_]+$/).test(value)

最后一位不能是下划线(?!.*?_$)

new RegExp(/^(?!_)(?!.*?_$)[A-Za-z_]+$/).test(value)

正则-以字母开头且仅包含字母数字以及下划线

new RegExp(/^[A-Za-z](?!.*?_$)[A-Za-z0-9_]+$/).test(value)

字符串中指定字符的数量

(str.match(/指定字符/g) || []).length

处理查询条件输入含有数据库的通配符

在JavaScript中,如果用户输入的查询条件包含数据库中使用的通配符(例如 % 或 _),你需要对这些字符进行转义,以避免SQL注入攻击

function escapeQueryValueForSQL(value) {
    // 对 '%' 和 '_' 进行转义
    return value.replace(/[%_]/g, (match) => {
    // 使用 '\\' 进行转义
        return '\\' + match;
    });
}

大数处理

  1. 数据转成字符串
  2. bignumber.js json-bigint

通过拼接的函数名调用函数

eval(funcName + '(' + arg + ')') // funcName:函数名 arg:参数

文件下载

使用 a 标签的 download 属性,同源才能触发下载,IE 不支持,移动端兼容性也不太好。

<a href="/path/to/file" download>Download</a>

// 或者 js 临时生成 a
function download(url) {
  const link = document.createElement('a')
  link.download = 'file name'
  link.href = 'url'

  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
}

前端安全浏览器打开新的标签页

看似平平无奇的打开页签,但是需要关注下 rel,如果要打开外链,建议设置为 noopener noreferrer,避免一些恶意网站通过 window.opener.location 重定向你的网站地址。window.open 方法同理。

js
// 高版本浏览器 rel 默认为 noopener,不过建议显示设置,兼容低版本。
<a target="_blank" rel="noopener noreferrer">...</a>

// window.open rel 默认为 opener,需要自己设置
window.open('https://baidu.com', '_blank', 'noopener,noreferrer')

// 以下有安全漏洞,打开的新页签可以通过 window.opener.location 重定向你的网站
<a target="_blank" rel="opener">...</a>
window.opener.location = 'http://fake.website.here';

URL 参数转化为对象

获取 url 参数有个热门的库 query-string,如果不想使用的话,可以通过 URLSearchParams API 实现。

const getUrlParams = (query) =>
  Array.from(new URLSearchParams(query)).reduce(
    (p, [k, v]) =>
      Object.assign({}, p, { [k]: p[k] ? (Array.isArray(p[k]) ? p[k] : [p[k]]).concat(v) : v }),
    {}
  )
  
// 获取 query 参数
getUrlParams(location.query)
// { a: ['1', '4'], b: '2', c: '3' }
getUrlParams('?a=1&b=2&c=3&a=4')

// 获取 hash 参数
getUrlParams(location.hash.split('?')[1])

清理数组中所有假值( 0falsenullundefined''NaN

const arr = [ 0, 1, false, 2, '', 3 ]; 
const cleanedArray = arr.filter(Boolean);

将 NodeList 转换为数组

const nodesArray = [ ...document.querySelectorAll('div') ];

const nodesArray = Array.from(document.querySelectorAll('div'));

取两个数组的交集

const arr1 = [1, 2, 3, 4];
const arr2 = [2, 4, 6, 8];

// 取两个数组中公共的元素
const intersection = arr1.filter(value => arr2.includes(value));

// 结果: intersection = [2, 4]

使用变量作为对象的键

const dynamicKey = 'name';
const value = '张三';

// 使用一个动态的变量作为 key
const obj = {[dynamicKey]: value};

// 结果: obj = { name: '张三' }

离开页面弹出确认对话框

当用户离开页面时会弹出一个确认对话框,一般用于防止用户因未保存更改就关闭页面而导致数据丢失。

window.onbeforeunload = () => '你确定要离开吗?';

切换元素的 class

const element = document.querySelector('.my-element');

const toggleClass = (el, className) => el.classList.toggle(className);

toggleClass(element, 'active');

 提取文件扩展名

先找到最后一次出现点号 (.) 位置,然后截取从该位置到末尾的字符串。位运算符 (>>>) 确保了即使未找到点号 (.) ,操作也是安全的,因为在这种情况下仍然会返回一个空字符串。

const fileName = 'example.png';

const getFileExtension = str => str.slice(((str.lastIndexOf(".") - 1) >>> 0) + 2);

// 结果: getFileExtension = 'png'

tesseract.js

分析出图片上的文字

<template>
    <div><Input v-model="value" /></div>
    <div class="img-container">
        <img v-for="img in filterImgs" :src="img.src" />
    </div>
</template>

<script setup lang="ts">
import { ref, onMounted, corputed } from 'vue';
import { Input } from "ant-design-vue';
import Tesseract fron 'tesseract.js';

const requestImgs=() => {
    return [
        "http://127.8.0.1:3006/img1.png'
        "http://127.8.0.1:3086/img2.png'
        "http://127.8.0.1:3006/img3.png',
        'http://127.8.0.1:3006/img4.png',
        'http://127.8.0.1:3006/img5.png'
    ].map(img => ({
        name: img,
        src: img,
    }));
});

const value = ref("');
const allImgs = ref<any[]>([]);
//过滤出符合条件的图片
const filterImgs = computed(() => 
    allImgs.value.filter(({ keyword })=> keyword.includes(value.value))
);
const resolveImg = (obj: any) => {
    const o = { ...obj };
    return Tesseract.recognize(o.src, 'eng')
      .then(({ data: { text }}) => {
        //成功则把 text 放在 keyword 中
        return { ...o, keyword: text };
      })
     .catch(()=>{
        //失败则把 src 放 keyword 中
        return{ ...o, keyword: o.src };
     });
};

const getImgs =async()=> {
    const res = requestImgs();
    //解析图片文字
    allImgs.value = await Promise.all(res.map(o > resolveImg(o)));console.log(allIrgs.value);
};

onMounted(()=>{
    getImgs();
}
</script>

Vue2

组件中定义的方法会覆盖mixins中定义的方法,即组件中的同名方法优先级高于mixins中的同名方法

Vue3

将es6打包成es5 解决低版本浏览器兼容问题

为了将ES6代码转换为ES5,可以使用Babel。Vite是一个基于ESM的构建工具,它可以快速地构建现代Web应用程序。下面是在Vite中配置Babel以将ES6转换为ES5的步骤:

1.安装依赖

npm install @babel/core @babel/preset-env -D

2.在项目根目录下创建.babelrc文件,并添加以下内容:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "chrome": "58",
          "ie": "11"
        }
      }
    ]
  ]
}

3.在vite.config.js文件中添加以下内容:

export default defineConfig({
  build: {
    target: "es2015",
    minify: "terser",
    sourcemap: false,
    rollupOptions: {
      output: {
        format: "es",
        manualChunks: undefined,
      },
      input: {
        main: "./index.html",
      },
      plugins: [],
    }
  },
});

VueUse

VueUse 是一个基于 Composition API 的实用函数集合。通俗的来说,这就是一个工具函数包,它可以帮助你快速实现一些常见的功能,免得你自己去写,解决重复的工作内容。以及进行了基于 Composition API 的封装。让你在 vue3 中更加得心应手。

useElementBounding

动态获取元素的宽高、位置等属性

<template>
  <div ref="el" />
</template>

<script>
import { ref } from 'vue'
import { useElementBounding } from '@vueuse/core'

export default {
  setup() {
    const el = ref(null)
    const { x, y, top, right, bottom, left, width, height } = useElementBounding(el)

    return {
      el,
      /* ... */
    }
  }
}
</script>

useDraggable

使元素可拖拽。

<script setup lang="ts">
import { ref } from 'vue'
import { useDraggable } from '@vueuse/core'

const el = ref<HTMLElement | null>(null)

// `style` will be a helper computed for `left: ?px; top: ?px;`
const { x, y, style } = useDraggable(el, {
  initialValue: { x: 40, y: 40 },
})
</script>

<template>
  <div ref="el" :style="style" style="position: fixed">
    Drag me! I am at {{x}}, {{y}}
  </div>
</template>

Vue UI

Ant Design Vue(1.7.8)

upload上传

beforeUpload支持返回一个 Promise 对象,Promise 对象 reject 时则停止上传(return Promise.reject())。注意:使用return false不能停止上传

通过accept=".geojson"指定上传的文件必须为geojson格式

 <a-upload
    name="file"
    accept=".geojson"
>
    ......
 </a-upload>

select根据label进行搜索

1、通过showSearch属性开启搜索功能,再通过设置optionFilterProp="label"结合给每个a-select-option标签添加:label="item.layerName"属性来实现

<a-select
    show-search
    optionFilterProp="label"
 >
    <a-select-option
      v-for="(item, i) in layerOptions"
      :key="item.id"
      :value="item.id"
      :label="item.layerName"
    >
      {{ item.layerName }}
    </a-select-option>
</a-select>

2、通过filterOption方法进行搜索过滤。这个方法接收输入值和每个选项,然后比较选项的文本内容是否包含输入值,从而决定是否显示该选项。

<template>
  <a-select
    v-model="selectedValue"
    show-search
    placeholder="Select a option"
    :filter-option="filterOption"
  >
    <a-select-option v-for="option in options" :key="option.value" :value="option.value">
      {{ option.label }}
    </a-select-option>
  </a-select>
</template>
 
<script>
export default {
  data() {
    return {
      selectedValue: undefined,
      options: [
        { value: 'option1', label: 'Option 1' },
        { value: 'option2', label: 'Option 2' },
        { value: 'option3', label: 'Option 3' },
        // ...更多选项
      ],
    };
  },
  methods: {
    filterOption(input, option) {
      return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0;
    },
  },
};
</script>

tree-select 搜索

项目开发的过程中出现无法通过名称进行搜索,原因是treeNodeFilterProp默认值为'value',应改为'title'

<a-tree-select
    v-model="selectLayerGroup"
    style="width: 250px"
    show-search
    treeNodeFilterProp="title"
    :dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
    :tree-data="layerGroupList"
    :replace-fields="{ children: 'layerList', key: 'uuid', value: 'uuid', title: 'name' }"
    placeholder="Please select"
    tree-default-expand-all
    @select="changeLayerGroup"
    >
</a-tree-select>

解决 ant-design 输入框显示历史输入记录的问题

给 Form 表单添加 autocomplete ="off"

<a-form-model ref="ruleForm' :model="form" :label-col="labelCol" :wrapper-col="wrapperCol" autocomplete ="off">

实时监听a-input输入

通过@change获取的值是改变之前的值
通过@input能够实时获取改变之后的值

取消按钮点击效果

/deep/ [ant-click-animating-without-extra-node='true']::after {
  display: none;
}

给a-date-picker设置默认值 报TypeError: _vm.moment is not a function in Vuejs

  • 可以在data中定义并赋值一下moment(需验证)
 data: () => ({
      moment: moment
   })
  • 可以在methods中定义一下,将moment转为方法
methods: {
    moment
}

GIS

投影、坐标系

EPSG:4326 (WGS84)

EPSG:3857 (Pseudo-Mercator)

伪墨卡托投影,也被称为球体墨卡托,Web Mercator。最初使用EPSG:900913(Google的数字变形) 的非官方代码来代表它

Web地图服务规范(WMS、WMTS、TMS)

1.概况

Web地图服务规范包括WMS(网络地图服务)、WMTS(网络地图瓦片服务)、TMS(瓦片地图服务)等。WMTS服务和WMS服务都是由开发地理信息联盟(OGC)指定。其不同在于,WMTS服务采用缓存技术能够缓解WebGis服务器端数据处理的压力。TMS服务由开源空间信息基金会(OSGEO)指定。TMS与WMTS服务的差异主要体现在:

  • TMS是纯Restful,而WMTS可以有KVP、SOAP和Restful三种。
  • TMS瓦片是正方形的,而WMTS是矩形的(正方形是特殊的矩形)
  • 在纵轴方向上方向相反,TMS瓦片以左下角为原点,WMTS瓦片以左上角为原点
  • WMTS中对应的不同比例尺瓦片可以尺寸不同。

在地图服务中,为了处理方便,瓦片均采用正方形,一般像素为256(居多)或者512。

这里需要说明一下,256的意思是对应瓦片(图片)是由横方向有256行像素点,纵方向256个像素,也就是说,对于256的一个瓦片,总共有256^2=65536个像素。由此也可以算出一个瓦片的大小,一个像素有RBG三个颜色通道组成,每个通道范围为0~255(即三个字节),也就是说一个像素为9个字节,那么一个瓦片的大小是589,824B。不过jpg,png等都采用了压缩技术,实际大小要小很多。

2. 基本知识

本次记录不是简单的介绍一下这三个协议,而是要要对地址做一下解析说明,这样可以更好的理解Web服务协议。为了更好的说明,这里需要引入几个知识点,以EPSG:4326网格集为例。

2.1 地球平面化

地球是一个球体,如果把其展开,可以近似看成一个矩形。宽近似为周长的一半。所以就有了如下图:

2021041517063989.jpg

2.2 瓦片形成

为了满足TMS协议,瓦片采用正方形存放,所以,在切瓦片之前应该在中间先切一刀,保证基础为正方向。

20210415170652346.jpg

此时沿着每个区域横方向和总方向各切一刀,这样就出现8块图片,也就是0级别有8块瓦片。

20210415170702228.jpg

按照如此方式,每增加一个层级,每个瓦片就被分成4分。所以,层级1有8*4=32块瓦片。

20210415170714982.png

而在实际tif切瓦片的过程中,他是从最高一级往下切。比如一个tif文件可切成0-10级瓦片,他会先切10级瓦片,然后通过融合的方式把4×4的瓦片合并为一张瓦片,依次类推。

3. Web地图服务地址解析

地图服务提供的服务地址分别为:

  • WMS:http://ip:port/geo/service/wms?layer={layername}&TILED=true

  • WMTS:http://ip:port/geo/service/wmts?layer={layername}

  • TMS:http://ip:port/geo/service/tms/1.0.0/{layername}@EPSG:4326@png

然而,如果要获取一个瓦片,需要如下的地址:

  • WMS:http://ip:port/geo/service/wms?layer={layname}&TILED=true&version=1.3.0&request=GetMap&format=image/png&transparent=true&width=256&heigh=256&CRS=EPSG:4326&styles=&bbox=minx,miny,maxx,maxy

  • WMTS:http://ip:port/geo/service/wmts?layer={layname}&tilematrixset=EPSG:4326&service=WMTS&Request=GetTile&Version=1.0.0&Format=image/png&TileMatrix=EPSG:4326:2&TileCol=5&TileRow=1

  • TMS:http://ip:port/geo/service/tms/1.0.0/{layname}@EPSG:4326@jpeg/2/6/1.jpeg

我们知道,一个瓦片是通过层级范围、瓦片X坐标和瓦片Y坐标,对于WMTS和TMS,我们很容易找到这三个要素,然而WMS就没有那么容易了。下面就做一一分析。

3.1 WMTS

通过上的地址我们分析到,WMTS服务三要素体现在TileMatrix=EPSG:4326:2&TileCol=5&TileRow=1。其中层级范围为2(EPSG:4326:2),瓦片X坐标为TileCol=5,瓦片Y坐标为TileRow=1。现在我们取层级范围为3,第三列第六行的瓦片,那么参数应该就是(注意瓦片编号从0开始):

TileMatrix=EPSG:4326:3&TileCol=2&TileRow=5

3.2 TMS

通过上的地址我们分析到,TMS服务三要素体现在/2/6/1.jpeg,其中2为层级范围,6为瓦片X坐标,1为瓦片Y坐标。由上面说到,TMS的原点在左下角,所以如果取到和上面WMTS一样的瓦片,地址应该为:

/3/2/2.jpeg

那么WMTS和TMS的关系是什么呢? 层及范围和X坐标(也就是行号一致),Y坐标是,这两个的Y坐标相加等于2^(层级范围)-1,上面层级范围为3,所以两者相加等于7即可。

3.3 WMS

通过上面观察我们发现这个地址中并没有这三个要素,但是我们发现有个参数是bbox.这里传入了瓦片的左下角和右上角的坐标。那么通过这两个坐标就可以计算出三要素。需要注意的事,参数如果传入有偏差,将不会反返回正确结果。那么我们现在在算一下上面相同的瓦片的坐标该是多少呢?

  • 首先,层级范围为3,应该有16列,8行瓦片。

ca7e742c623f4400b1bb8684ce1e996a.png

  • 那么我们现在计算黄色瓦片的左下角和右上角的坐标。对于X轴,总共有16个瓦片,长度为360°,所以每个瓦片的角度为22.5°。
    • 对于X轴方向:最左边的坐标为-180°,所以黄色块的左下角X坐标为-180+22.5*2=-135;右上角X坐标为加一个瓦片的度数,为-112.5。
    • 对于Y轴方向:最下边的坐标为-90度,所以黄色块的左下角Y坐标为-90+22.25*2=-45.5;右上角Y坐标为加一个瓦片的度数,为-56.25。

所以通过上面分析,WMS的参数中的Bbox参数应该为:

bbox=-135,-67.5,-112.5,-56.25

maptalks

删除文本图标

 let layerDraw = this.mapView.getLayer('drawLayer') // 文本图标所在图层
 const marker = this.selectDraw.marker // 文本图标
 if (marker) {
    marker.endEditText && marker.endEditText() // 文本取消编辑状态 不然编辑时生成的div无法删除
    setTimeout(() => {
        layerDraw.removeMarker(marker._id) // 根据id删除文本图标
    }, 100)
 }

修改绘制图形样式(线条颜色、填充色、透明度)

图形非'MultiPolygon', 'Polygon'类型,geom.setSymbol传递的参数只能是一个对象,而非数组(数组中包含字体样式)。

if (['MultiPolygon', 'Polygon'].includes(this.drawType)) {
  geom.setSymbol([
    {
      lineColor: this.lineColor,
      polygonOpacity: this.fillOpacity,
      polygonFill: this.fillColor
    },
    {
      textName: this.selectedGeomName,
      textFill: this.textColor,
      textSize: this.textSize,
      textHaloFill: this.textHaloFill,
      textHaloRadius: this.textHaloRadius
    }
  ])
} else {
  geom.setSymbol({
    lineColor: this.lineColor,
    polygonOpacity: this.fillOpacity,
    polygonFill: this.fillColor
  })
}

根据json数据在图层上加载图形

json数据结构

{
  "feature": {
    "type": "Feature",
    "geometry": {
      "type": "MultiPolygon",
      "coordinates": [
        [
          [
            [119.58981409031688, 32.2704284218924],
            [119.59075316842927, 32.27022231058618],
            [119.5906478037065, 32.26988161729536],
            [119.5912705998392, 32.269742546572296],
            [119.59137596254115, 32.27008557165805],
            [119.59220909108149, 32.2699024541709],
            [119.59147524640426, 32.267482318068325],
            [119.58929260734978, 32.267967403503405],
            [119.58908354249076, 32.26801295340617],
            [119.58981409031688, 32.2704284218924]
          ]
        ]
      ]
    },
    "properties": {
      "OBJECTID_1": 1,
      "OBJECTID": 0,
      "Entity": "Group",
      "Layer": "JMD",
      "Color": "#0000ff",
      "Linetype": "CONTINUOUS",
      "Elevation": 0,
      "LineWt": 0,
      "RefName": "制造部(预舾装及分段堆场)",
      "Shape_Leng": 1098.11139164,
      "Shape_Le_1": 1098.11139164,
      "Shape_Area": 62116.857064
    }
  },
  "options": {},
  "symbol": [
    { "lineColor": "#948250", "polygonOpacity": 1, "polygonFill": "#0000ff" },
    {
      "textName": "制造部(预舾装及分段堆场)",
      "textFill": "black",
      "textSize": 12,
      "textHaloFill": "#fff",
      "textHaloRadius": 2
    }
  ]
}

加载图形

var vlayer = new BDLayers.Lib.Layer.CBVectorLayer('93e284550890404981eb01ab239904bf'); // 图层用于加载图形
mapView.addCustomLayers(vlayer);

data.geoDataList.forEach((geoJsonStr, index) => {
    let geoJson = JSON.parse(geoJsonStr)
    geoJson.options = {} // 这段代码一定要加上 因为添加或编辑图形 保存时会给图形json数据加上options:{draggable:false, visible:false} 从而导致图形不展示
    const geom = new BDLayers.Lib.Overlays.FromGeoJson(geoJson) // 根据json数据生成对应的geom图形
    vlayer.addGeometry(geom)
})