首个Vue3项目边写边学边记

目录

[一、HTML 篇](#一、HTML 篇)

[1、设置 loading 状态](#1、设置 loading 状态)

2、判断文件选中操作

[二、TypeScript 篇](#二、TypeScript 篇)

[1、function 之 void](#1、function 之 void)

[2、Promise 之 finally](#2、Promise 之 finally)

[3、TS 类型约束](#3、TS 类型约束)

4、依赖包无TS类型声明文件

5、TS校验接口参数问题

[三、CSS 篇](#三、CSS 篇)

1、gap

2、样式穿透


背景:

#创作灵感#

  • 记录工作实践、项目所学
  • 写技术笔记巩固新学知识点

由于上一家公司项目全都是使用 Vue2 和 element 组件库 进行的开发,所以一直没有真正使用 Vue3 和 ElementPlus 开发过项目,虽说有接触简单学习了解过,但跟实际上手开发 Vue3 和 ElementPlus 项目肯定还是有差别的。但好巧不巧,新入职公司接手的第一个项目就是用 Vue3 ➕ ElementPlus 开发,而且项目还是用 pnpm 管理,这些都是头一次接触,但是没办法也只能硬着头皮必须顶上去,想想以前不也都靠 "度娘" 撑过来了,更何况现在还有了技术更发达的 AI 可以辅助开发了,相信自己 "船到桥头自然直",加油干就完了!


一、HTML 篇

1、设置 loading 状态

问题:

怎么给 img 标签添加 loading 加载状态呢?用的是 elementplus 组件库,现在直接加:loading 不生效,img 标签外层套的 el-tooltip 标签,可以在这上面加吗?给我个实现方案


Element Plus 图片加载 loading 实现方案

问题原因

原生 img 没有 :loading 属性,el-loading 指令 / 组件只能加在 有固定宽高、可定位的容器元素 上,直接绑 img 不生效;外层 el-tooltip 是悬浮提示组件,不能直接绑定 loading 指令,需要 给图片包一层固定宽高的容器,在容器上加 loading。

方案一:el-loading 指令(推荐,简单易用)

给 img 外层套 div 容器,容器设置和图片一致的宽高,在容器上使用 v-loading 指令,加载完成后关闭 loading

html 复制代码
<template>
  <!-- 外层tooltip不动,tooltip内部包一层loading容器 -->
  <el-tooltip content="提示文字" placement="top">
    <!-- 加载容器:必须设置宽高,和图片尺寸保持一致 -->
    <div 
      v-loading="isImgLoading" 
      element-loading-text="图片加载中"
      style="width: 200px; height: 200px;"
    >
      <img 
        src="你的图片地址" 
        alt="图片"
        @load="isImgLoading = false"
        @error="isImgLoading = false"
        style="width:100%;height:100%;object-fit:cover"
      />
    </div>
  </el-tooltip>
</template>

<script setup>
import { ref } from 'vue'
// 初始化开启加载
const isImgLoading = ref(true)
</script>

方案二:el-loading 组件形式(自定义更强)

使用独立 <el-loading> 组件包裹容器,适合需要自定义加载图标、遮罩样式的场景

html 复制代码
<template>
  <el-tooltip content="提示文字">
    <div style="width: 200px; height: 200px; position: relative;">
      <el-loading 
        v-model="isImgLoading"
        text="加载中..."
        background="rgba(255,255,255,0.7)"
      >
        <img 
          src="图片地址" 
          alt=""
          @load="isImgLoading = false"
          @error="isImgLoading = false"
          style="width:100%;height:100%"
        />
      </el-loading>
    </div>
  </el-tooltip>
</template>

<script setup>
import { ref } from 'vue'
const isImgLoading = ref(true)
</script>

方案三:图片不确定宽高自适应场景

如果图片宽高不固定,容器设置为 inline-block 适配图片尺寸,配合默认占位图:

html 复制代码
<el-tooltip content="提示">
  <div 
    v-loading="isImgLoading"
    style="display: inline-block;"
  >
    <!-- 加载前显示默认占位图 -->
    <img
      :src="imgSrc"
      alt=""
      @load="isImgLoading = false"
      @error="isImgLoading = false"
      width="auto"
    />
  </div>
</el-tooltip>

关键注意点

  1. el-tooltip 不能直接加 loading:tooltip 只是悬浮层容器,渲染逻辑特殊,loading 必须加在 tooltip 内部的图片父容器
  2. 必须监听@load和@error:无论加载成功失败都关闭 loading,避免永久加载状态
  3. 容器一定要设置宽高:否则 loading 遮罩没有定位基准,会全屏遮罩
  4. 全局配置 loading:可以在 main.js 全局修改 loading 样式,统一项目加载效果
javascript 复制代码
// main.js 全局配置el-loading
import { ElLoading } from 'element-plus'
app.use(ElLoading, {
  text: '图片加载中',
  spinner: '自定义图标类名'
})

问题:

不能给 img 标签设定宽高,然后设置 v-loading 吗?

结论:仅给 img 设宽高,直接绑 v-loading 依然大概率不生效

核心两个原因

1、img 是行内替换元素

v-loading 的遮罩是 绝对定位 ,需要父级 / 自身 position: relative 做定位基准; 原生 img 默认是 display: inline,哪怕写了宽高,不主动改成 block/inline-block + relative,遮罩会直接跑出图片区域,甚至全屏显示 loading。

2、v-loading 会把遮罩插入当前元素的 子节点 里

<img> 是 自闭合标签,没有子元素,loading 的 DOM 结构根本无法插入到 img 内部,这是最根本的限制。 div 这类容器可以内部插入遮罩 DOM,img 不能放子标签,指令无从挂载内部遮罩。


如果你坚持想直接写在 img 上:必须补全 3 个强制样式(不推荐,有坑)

html 复制代码
<el-tooltip content="提示">
  <img
    v-loading="isLoading"
    src="xxx"
    style="
      width: 200px;
      height:200px;
      display: inline-block; /* 转为行内块 */
      position: relative; /* 给遮罩定位基准 */
    "
    @load="isLoading=false"
    @error="isLoading=false"
  />
</el-tooltip>

缺陷:部分浏览器 / 版本 el-plus,自闭合标签挂载 v-loading 会渲染异常,loading 转圈偏移、消失不掉,不稳定。


最优最简方案(只套一层空 div,不影响布局)

只包一个 inline-block 的容器,容器自适应图片大小,不需要固定宽高,完全不改动原图样式,el-tooltip 套在最外层即可:

html 复制代码
<template>
  <el-tooltip content="悬浮提示">
    <!-- 容器自适应图片尺寸,不破坏原有布局 -->
    <div 
      v-loading="isImgLoading"
      style="display: inline-block;"
    >
      <img 
        :src="imgUrl"
        alt="图片"
        @load="isImgLoading = false"
        @error="isImgLoading = false"
      />
    </div>
  </el-tooltip>
</template>

<script setup>
import { ref } from 'vue'
const isImgLoading = ref(true)
const imgUrl = ref('你的图片地址')
</script>
  • display:inline-block 容器会自动贴合 img 的宽高,不用手动写尺寸
  • loading 挂载在 div 容器,DOM 结构合法,遮罩永远精准盖住图片
  • el-tooltip 包裹整个容器,悬浮提示不受任何影响

额外备选:直接用 Element Plus 自带的 el-image 组件

el-image 原生自带加载 loading、加载失败兜底,完美适配 tooltip,无需自己写加载逻辑:

html 复制代码
<el-tooltip content="提示">
  <el-image 
    src="图片地址"
    fit="cover"
  />
</el-tooltip>

2、判断文件选中操作

问题:

前端能实现点击页面按钮,弹出选择文件夹弹窗,然后点确定后调用后端接口这样的操作吗?


结论先行

纯前端浏览器环境,无法直接弹出系统原生的「选择文件夹」弹窗并获取本地文件夹路径,受浏览器安全沙箱限制;但有两套可行方案实现需求,分场景选择:

一、现代浏览器原生方案(推荐,无需插件)

HTML5 提供了 webkitdirectory 文件夹选择属性 ,可以弹出文件选择框、限制只能选择整个文件夹,能拿到文件夹内所有文件,再把文件 / 文件夹信息传给后端

1. 前端代码示例

html 复制代码
<!-- 隐藏input,按钮点击触发它 -->
<button id="selectFolderBtn">选择文件夹</button>
<input type="file" id="folderInput" webkitdirectory multiple style="display:none">

<script>
const btn = document.getElementById('selectFolderBtn')
const input = document.getElementById('folderInput')

btn.onclick = () => input.click()

// 选中文件夹后的回调
input.onchange = async (e) => {
  const files = Array.from(e.target.files)
  if(!files.length) return

  // files 里包含该文件夹下所有子文件,可读取文件名、相对路径、文件内容
  const formData = new FormData()
  files.forEach(file => {
    // 关键:保留文件在文件夹内的相对路径,后端可还原目录结构
    formData.append('files', file, file.webkitRelativePath)
  })

  // 调用后端接口上传/提交文件夹数据
  const res = await fetch('/api/upload-folder', {
    method: 'POST',
    body: formData
  })
  console.log('提交完成', await res.json())
}
</script>

优缺点

✅ 纯前端、无第三方依赖、Chrome / Edge / Firefox / 新版 Safari 全支持

❌ 拿不到本地绝对物理路径(浏览器安全禁止,只能获取相对目录);只能上传文件夹内文件,不能直接把本地文件夹地址发给后端

二、想要获取本地文件夹绝对路径(仅本地环境可用)

只有 非浏览器环境才能读取系统文件夹真实路径,适用于 Electron 桌面端、本地客户端:

1. Electron 桌面应用(前端技术写桌面程序)

Electron 可以调用 Node.js 的 dialog.showOpenDialog 直接弹出 系统原生文件夹选择框,拿到完整本地路径,再传给内置后端或远程接口

javascript 复制代码
// 主进程代码
const { dialog } = require('electron')
// 渲染进程通知主进程打开文件夹选择
dialog.showOpenDialog({
  properties: ['openDirectory'] // 只选文件夹
}).then(result => {
  const folderPath = result.filePaths[0] // 拿到本地绝对路径 C:/xxx/xxx
  // 把路径发送给渲染进程,由渲染进程调用后端接口
})

适用场景:内部管理系统、本地工具类软件

三、区分两种需求帮你选型

  1. 公网网页、给普通用户使用 :只能用第一种 webkitdirectory 上传文件夹内文件,无法获取本地路径
  2. 内部工具、只在自己电脑 / 公司电脑使用:打包成 Electron 桌面程序,随意获取文件夹本地路径
  3. 老旧 IE 浏览器:无原生文件夹选择,只能用 ActiveX(已废弃,不推荐)

补充后端接收说明

后端接收 FormData 格式的文件,通过文件名里的 webkitRelativePath 可以还原用户选择的文件夹目录结构,保存到服务器指定文件夹。

3、


二、TypeScript篇

1、function 之 void

问题:

function handleOpen(): void { } 的 void 啥意思?


TypeScript 里 void 的含义

void 写在函数返回值位置,代表 这个函数 没有返回值、不返回 任何数据。

1. 基础解释

TypeScript 复制代码
// 声明:调用 handleOpen 后,不会返回任何东西
function handleOpen(): void { }
  • 普通函数默认可以 return 123、return 'abc' 把值抛出去给外面接收
  • 标记 void:约定 本函数不写 return 返回任何有效值 ,只能:
    1. 不写 return
    2. 只写单独 return;(不带值)

2. 和不写返回值的区别

JS 原生(无类型)

javascript 复制代码
// JS 不写返回,默认自动返回 undefined
function handleOpen() {}
const res = handleOpen() // res === undefined

TS 写 void

TypeScript 复制代码
function handleOpen(): void {}
let a = handleOpen()
a = 1 // 报错:void 类型不能赋值数字

TS 用 void 限制语义:我设计这个函数就是只执行动作,不产出返回值,禁止你去接收、使用它的返回结果。

3. 小误区:void 函数也能写 return,但不能带值

TypeScript 复制代码
// 合法,只是中断函数,没有返回数据
function fn(): void {
  if (xxx) return
  console.log('执行')
}

// 非法!不能返回具体数值
function fn(): void {
  return 123 // TS直接报错
}

4. 常见场景

点击事件、打开弹窗 这类 只做动作、不需要返回数据 的函数,都会标 void,就是你代码里 handleOpen 打开弹窗这类操作的标准写法。


2、Promise 之 finally

问题:

.finally(() => { }) 这个 finally 是啥?


Promise 的 .finally() 方法

它是 异步 Promise 专属 的 回调方法,写在 .then()、.catch() 后面

finally 直译:最终、无论如何最后都要执行

核心特点

不管前面的异步请求 成功走了 then ,还是 失败报错走了 catch ,finally 里面的代码 一定会执行

基础结构

javascript 复制代码
fetch("/api")
  .then(res => console.log('请求成功'))
  .catch(err => console.log('请求失败'))
  .finally(() => {
    // 请求成功 OR 失败,这里的代码必定执行
    console.log('请求结束,收尾工作')
  })

最常用场景(日常开发高频)

加载 loading 动画:发送请求时打开 loading,不管请求成功还是失败,最后都要关闭 loading

javascript 复制代码
// 打开加载
loading.show()

axios.get('/getData')
  .then(data => setData(data))
  .catch(e => showError(e))
  .finally(() => {
    loading.hide() // 成功失败都会关掉loading,不用在then和catch各写一次关闭
  })

补充细节

  1. finally 的回调 没有参数,拿不到成功的数据,也拿不到错误信息,它只负责收尾清理
  2. 不会改变 Promise 的返回值,不影响前面 then 接收到的数据
  3. 同步代码没有 finally,只配合异步 Promise 使用

3、TS 类型约束

<el-upload>标签上的 :before-upload 红色波浪线提示了,帮我看一下怎么回事?

bash 复制代码
Type '(file: any) => true | "xls"' is not assignable to type '(rawFile: UploadRawFile) => Awaitable<boolean | void | File | Blob>'.
Type 'true | "xls"' is not assignable to type 'Awaitable<boolean | void | File | Blob>'.
Type '"xls"' is not assignable to type 'Awaitable<boolean | void | File | Blob>'. ts-plugin(2322)

upload.vue.d.ts(274, 14): The expected type comes from property 'beforeUpload' which is declared here on type 'Partial<{ readonly data: unknown; readonly disabled: boolean; readonly drag: boolean; readonly multiple: boolean; readonly name: string; readonly onChange: (uploadFile: UploadFile, uploadFiles: UploadFiles) => void; ... 15 more ...; readonly onExceed: (files: File[], uploadFiles: UploadUserFile[]) => void; }> & Omit...'

(property) beforeUpload?: (rawFile: UploadRawFile) => Awaitable<boolean | void | File | Blob>

问题原因

Element Plus 的 before-upload 钩子 有严格的 TS 类型约束 : 函数必须返回 Promise<boolean | File | Blob> / boolean / void,不能返回字符串 。 你的 handleBeforeUpload 函数里大概率存在:符合条件返回 true,不符合直接返回了 "xls" 这类字符串,TS 校验报错。

报错翻译:

类型 (file: any) => true | "xls" 不能赋值给 beforeUpload 要求的类型 不能把字符串"xls"赋值给布尔 / 文件类型

修复方案

1. 修改 handleBeforeUpload 函数(核心)

只允许返回布尔值、Promise、或者不 return(void),校验失败只做提示,返回 false 拒绝上传 示例修正写法:

TypeScript 复制代码
const handleBeforeUpload = (rawFile: UploadRawFile) => {
  // 拿到文件后缀
  const suffix = rawFile.name.split('.').pop()
  const allowType = ['xlsx', 'xls']

  if (allowType.includes(suffix!)) {
    return true // 符合格式,放行上传
  } else {
    ElMessage.error('只能上传 xls/xlsx 格式文件')
    return false // 格式错误,拒绝上传,不要return "xls"字符串
  }
}

2. 补充细节

  1. 删掉函数里所有返回字符串的逻辑,校验失败 只提示 + 返回 false
  2. 可以给函数补全 TS 类型 (rawFile: UploadRawFile): boolean 更规范
  3. 你标签上已经写了 accept=".xlsx,.xls",前端双重校验更稳妥

3. 补充如果是异步校验

如果函数内部有异步操作,要返回 Promise:

TypeScript 复制代码
const handleBeforeUpload = async (rawFile: UploadRawFile) => {
  // 异步逻辑...
  return true
}

改完函数,红色波浪线就会消失。

4、依赖包无TS类型声明文件

引入树组件 vue3-treeselect,为何红色波浪线提示了?我明明 pnpm install -w vue3-treeselect,下载了,并且 package.json 的 dependencies 里面也有了"vue3-treeselect":"^0.1.10",帮我看一下怎么回事?

bash 复制代码
Could not find a declaration file for module 'vue3-treeselect'.
'D:/GitBlit/Program/WEB/node_modules/.pnpm/vue3-treeselect@0.1.10_vue@3.5.13_typescript@5.8.3_/node_modules/vue3-treeselect/dist/vue3-treeselect.common.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/vue3-treeselect` if it exists or add a new declaration (.d.ts) file containing `declare module 'vue3-treeselect';` ts-plugin(7016)

逐句翻译

英文原文 中文翻译
Could not find a declaration file for module 'vue3-treeselect'. 找不到模块 "vue3-treeselect" 的声明文件。
'D:/Git/node_modules/vue3-treeselect/dist/vue3-treeselect.common.js' implicitly has an 'any' type. 路径 D:/Git/node_modules/vue3-treeselect/dist/vue3-treeselect.common.js 隐式具有 "any" 类型。
Try npm i --save-dev @types/vue3-treeselectif it exists or add a new declaration (.d.ts) file containingdeclare module 'vue3-treeselect'; ts-plugin(7016) 如果存在 @types/vue3-treeselect 类型包,请尝试执行 npm i --save-dev @types/vue3-treeselect;或者新建一个声明文件(.d.ts),内容为 declare module 'vue3-treeselect';。错误码:ts-plugin (7016)

报错原因

vue3-treeselect 这个包 本身没有自带 TypeScript 类型声明文件(.d.ts) ,并且 npm 也没有官方维护的 @types/vue3-treeselect 类型包,所以 TS 找不到模块类型定义,抛出 7016 的找不到声明文件错误,和包有没有安装成功无关。

两种解决办法,推荐第一种

方案 1:新建类型声明文件(一劳永逸,规范 TS 项目)

1、在你的项目 src 目录下,新建 typings 或者直接新建 src/shims-vue3-treeselect.d.ts 文件

2、文件内写入模块声明,告诉 TS 忽略这个包的类型检查:

TypeScript 复制代码
// shims-vue3-treeselect.d.ts
declare module 'vue3-treeselect' {
  import { DefineComponent } from 'vue'
  const TreeSelect: DefineComponent<any, any, any>
  export default TreeSelect
}

declare module 'vue3-treeselect/dist/vue3-treeselect.css'

3、保存文件,重启 VSCode 的 TS 服务:快捷键 Ctrl+Shift+P 输入 Restart TypeScript Service 重启,红波浪线就会消失。

方案 2:单行忽略(临时快速解决)

直接在引入这一行的上方加 ts 忽略注释,跳过该行的类型校验:

TypeScript 复制代码
// @ts-ignore
import TreeSelect from "vue3-treeselect";
import "vue3-treeselect/dist/vue3-treeselect.css";

缺点:只对当前这一行生效,多处引入就要多次加忽略,不推荐长期使用。

补充排查小提示

  1. 确认 tsconfig.json 里的 include 配置包含了你新建的 d.ts 文件,一般默认包含src/**/* 就没问题
  2. 已经 pnpm 安装成功包,运行项目 pnpm dev 是可以正常运行组件的,只是 VSCode 的 TS 语法检查报错,不影响实际代码执行
  3. 这个包停止维护很久了,没有官方类型,只能自己补声明

5、TS校验接口参数问题

问题:

queryExcel 接口名红色波浪线提示了,帮我看一下原因

bash 复制代码
Expected 1 arguments, but got 0. ts-plugin(2554)
ban.ts(76, 28): An argument for 'params' was not provided.
(alias) queryExcel(params: any): Promise<AxiosResponse<any, any>>
import queryExcel

逐句翻译与说明

英文原文 中文翻译 补充说明
Expected 1 arguments, but got 0. ts-plugin(2554) 预期传入 1 个参数,但实际传入了 0 个。(错误码:ts-plugin (2554)) TypeScript 核心报错:调用函数时参数数量不匹配。
ban.ts(76, 28): An argument for 'params' was not provided. ban.ts 文件的第 76 行第 28 列:未提供 params 参数。 精准定位:错误发生的位置,明确指出缺失的参数名。
(alias) queryExcel(params: any): Promise<AxiosResponse<any, any>> (别名)queryExcel(params: any): Promise<AxiosResponse<any, any>> 函数的类型声明:queryExcel 接收一个 params 参数,返回值是一个 Axios 响应的 Promise 对象。
import queryExcel import queryExcel 提示该函数是通过 import 引入的。

报错原因

queryExcel 这个函数 定义时声明了必须传入一个名为 params 的必填参数 ,函数签名:queryExcel(params: any) 但是你调用时写的是 queryExcel(),一个参数都没传,TS 校验不通过,抛出 2554 缺少入参的错误。

两种解决方式

方式 1:传空对象(接口允许不传参时使用)

如果这个接口不需要传任何查询条件,调用时传一个空对象即可:

TypeScript 复制代码
queryExcel({})
  .then(res => {
    templateInfo.value = res.data.data;
    console.log("res", res.data.data);
  })

方式 2:修改接口定义,把参数改为可选(根源修复)

打开 queryExcel 的定义文件,给参数加问号 ?,变成可选参数:

TypeScript 复制代码
// 原代码类似这样
// export const queryExcel = (params: any) => request(...)

// 修改为可选参数
export const queryExcel = (params?: any) => request(...)

改完之后就可以直接 queryExcel() 不传参调用了。

补充说明

  • 报错只是 TypeScript 静态类型检查,如果你不传参直接运行项目,后端接口如果能接收空请求,代码其实可以跑通,只是 VSCode 会一直标红
  • 优先推荐 传空对象,不改动公共接口定义;只有你确定这个接口大部分场景都不需要传参,再去修改原函数的类型

问题:

接口参数 params 红色波浪线提示了

bash 复制代码
Parameter 'params' implicitly has an 'any' type. ts(7006)
(parameter) params: any

逐句翻译

英文原文 中文翻译
Parameter 'params' implicitly has an 'any' type. ts(7006) 参数 params 隐式具有 any 类型。(错误码:ts (7006))
(parameter) params: any (参数)params: any

报错原因

TS 报错 ts(7006):参数 params 没有写类型注解,TypeScript 自动隐式推断为 any 类型 ,你的项目开启了 noImplicitAny 强制类型检查,不允许隐式 any,因此标红。

三种解决方法

方法 1:临时直接标注 any(快速消红,简单粗暴)

给参数手动写上: any,明确声明类型,消除隐式 any 报错

TypeScript 复制代码
export function addUserApi(params: any) {
  return request({
    url: 'api/addUser',
    method: 'post',
    data: params,
  })
}

方法 2:定义专属接口类型(规范 TS 项目,推荐)

根据接口实际需要传递的字段,定义 TS 类型,替换 any,类型更安全

TypeScript 复制代码
// 先定义请求参数类型,放到顶部或单独的type文件

interface AddUserParams {
  id: number
  permIds: number[]
  // 新增接口的参数字段
}

// 使用自定义类型
export function addUserApi(params: AddUserParams) {
  return request({
    url: 'api/addUser',
    method: 'post',
    data: params,
  })
}

方法 3:修改 tsconfig.json 关闭隐式 any 校验(全局关闭,不推荐)

找到项目tsconfig.json,把noImplicitAny设为false,会全局取消这类报错

javascript 复制代码
{
  "compilerOptions": {
    "noImplicitAny": false
  }
}

建议

日常开发优先用方法 1 快速消除波浪线;长期维护项目建议用方法 2写具体类型,充分利用 TS 的类型校验。


6、


三、CSS 篇

1、gap

问:CSS 样式里的,gap: 8px; 啥意思?

gap: 8px 就是 设置元素之间的间距为 8 像素 ,主要用在 Flex、Grid、多列布局 里。

简单拆解:

  1. 作用 :控制 子元素之间的空隙,不用再给 margin 算间距,更简洁。
  2. 简写规则
    • gap: 8px:行、列间距都 = 8px
    • gap: 10px 5px:行间距 10px,列间距 5px
  3. 细分写法(等价)
    • row-gap: 8px 只改 行间距
    • column-gap: 8px 只改 列间距

举个直观例子

css 复制代码
.box {
  display: flex;
  flex-wrap: wrap;
  gap: 8px; /* 所有子盒子之间空出 8px */
}

里面每个子元素,上下、左右都会自动隔开 8px。


CSS布局中Gap与Padding的区别

【智能总结】

  • 区别:CSS 布局中,Gap 和 Padding 都用于 控制间距,但作用对象与场景不同,新手易混淆

1、Padding:内边距,作用于单个元素,控制内容与边框距离,对任何元素生效,设置后会扩大元素盒模型,增加总尺寸

2、Gap:元素间距,仅适用于 Flex 或 Grid 布局容器,控制 子元素 间空隙,不影响元素自身尺寸

  • 应用场景

1、Padding:适合 单个 元素

2、Gap:适合 多元素 排列

掌握此区别可精准控制布局间距。


【原文】

在 CSS 布局中,Gap 和 Padding 都用于 控制间距,但作用对象与场景不同。新手易混淆。

Padding 是 内边距 ,作用于 单个元素,控制其内容与边框的距离。

Gap 是 元素间距,仅适用于 Flex 弹性布局 或 Grid 网格布局容器,控制子元素之间的空隙。简言之,Padding 是元素内部留白,Gap 是元素之间空隙。

Padding 对任何元素生效,设置后会扩大元素盒模型。例如,给 div 设置 padding:30px,其内容会距离边框各30px,且总尺寸增加------若原宽高200px,设置后实际宽高为260px。

Gap 是依赖 Flex 或 Grid 布局,在容器中设置后,子元素间会产生空隙,但不影响元素自身尺寸。比如两个200px 宽的子元素,设置 gap:30px 后,总占用宽度为430px,单个元素尺寸不变。

Padding 区域会继承背景色,被边框包裹,并扩大可点击区域,让按钮更饱满。

Gap 只是纯粹空白,不继承背景,不可点击,仅用于分隔子元素。

实际使用时,padding 适合单个元素内部留白,如 按钮、卡片,Gap 适合多元素排列间距,如网格卡片。掌握这一区别,就能精准控制布局间距,避免踩坑。


2、样式穿透

问题:

/deep/ 开头的 / ,红色波浪线提示了,帮我看一下

bash 复制代码
at-rule or selector expected css(css-ruleorselectorexpected)

预期 at 规则或选择器(css 错误码:css-ruleorselectorexpected)


补充说明

  • at-rule :指 CSS 中以 @ 开头的规则,比如 @media、@keyframes、@import 等。
  • selector :指 CSS 选择器,比如类选择器 .class、ID 选择器 #id、元素选择器 div 等。
  • 这个报错的意思是:你的 CSS 代码中,语法结构不正确,解析器在本该出现 @ 规则或选择器的位置,没有找到有效的内容,导致解析失败。比如你写的 /deep/ 或 ::v-deep 语法格式不对,就会触发这个错误。

常见错误示例 & 修正

css 复制代码
/* ❌ 错误写法(/deep/ 前面缺少选择器) */
/deep/ ::-webkit-scrollbar-thumb:hover {
  background: #aaa;
}

/* ✅ 正确写法(给 /deep/ 前面加上父选择器) */
.wrapper /deep/ ::-webkit-scrollbar-thumb:hover {
  background: #aaa;
}

/* ✅ 更推荐的写法(Vue3 推荐使用 :deep() 伪类) */
:deep(.wrapper) ::-webkit-scrollbar-thumb:hover {
  background: #aaa;
}

报错原因

  1. Vue3 已经 废弃了 /deep/ 深度穿透语法,VSCode 的 CSS 校验器不识别这个旧语法,直接标红报错;
  2. Vue3 推荐使用 :deep() 函数式写法做样式穿透,不再支持 /deep/、>>> 这类 Vue2 的老穿透符号。

修复方案(把所有 /deep/ 替换为 :deep())

改你两行代码:

css 复制代码
/* 原来的错误写法 */
/deep/ ::-webkit-scrollbar-thumb:hover {}

/* Vue3 正确写法 */
:deep(::webkit-scrollbar-thumb:hover) {

}

/* 第二行同理替换 */
/* /deep/ .el-upload__tip {} */
:deep(.el-upload__tip) {
  margin-top: 0;
  margin-left: 1.5vh;
  font-size: 1.5vh;
}

补充说明

1、你的 <style> 标签必须 带有 scoped 属性 ,:deep() 才会生效:

html 复制代码
<style scoped>
/* 这里写 :deep() 样式 */
</style>

2、只是 VSCode 编辑器语法标红,Vue2 项目里 /deep/ 运行是正常的;但 Vue3 项目必须换成 :deep(),既消除报错,又符合新版本规范。

3、滚动条伪类写在 :deep() 内,可以穿透 scoped 修改子组件 / 全局滚动条样式。


3、

相关推荐
brien919 分钟前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance34 分钟前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M41 分钟前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写43 分钟前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李44 分钟前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易1 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马1 小时前
前沿视界
前端
kyriewen1 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易2 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员
IT_陈寒2 小时前
Vue的这个响应式陷阱,差点让我加班到天亮
前端·人工智能·后端