vue3

SmartAdmin–VUE3

1.对象

1. 响应式对象reactive

reactive 是 Vue 3 核心 API,作用是把普通对象 / 数组转换成「响应式代理对象」:

  • 响应式的核心特性:当对象的属性(如 queryForm.keywords)发生变化时,Vue 能自动检测到变化,并更新页面上绑定该属性的视图(比如输入框内容、分页组件);
  • 如果直接用普通对象,修改字段后页面不会自动更新,这也是 Vue 做响应式的核心目的。
1
2
3
4
5
6
7
const queryFormState = {
noticeTypeId: undefined, //分类
pageNum: 1,
pageSize: PAGE_SIZE,
};
// 转换为响应式
const queryForm = reactive({ ...queryFormState });
  1. 初始状态定义queryFormState 是一个普通对象,存放查询表单的默认值(如分页、时间范围、筛选条件),作为表单的 “重置基准”;不推荐 const queryForm = reactive({ queryFormState});,因为这样会污染模板,修改响应式对象时,模板也会被同步修改。

  2. 响应式转换reactive({ ...queryFormState }) 通过浅拷贝把初始状态转换成 Vue 3 的响应式对象 queryForm,保证表单字段变化时触发视图更新;同时将queryFormState进行浅拷贝后进行复制,有利于保护原模板对象不受影响,重置表单时,方便进行初始化。

  3. 浅拷贝的坑(嵌套对象场景)
    3.1 如果 queryFormState 包含嵌套对象(比如 timeRange: { begin: null, end: null }),{ ... } 浅拷贝会导致嵌套层级失去响应式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 问题复现
// 包含嵌套对象的初始模板
const queryFormState = {
keywords: '',
// 嵌套对象:时间范围
timeRange: {
createTimeBegin: null,
createTimeEnd: null
},
pageNum: 1
};

// 浅拷贝 + 响应式(你的原有写法)
const queryForm = reactive({ ...queryFormState });

3.2 浅拷贝({ ... })只会拷贝第一层字段,嵌套的 timeRange 本质是 “引用地址”,所以 queryForm.timeRangequeryFormState.timeRange 指向同一个对象

1
2
3
4
5
// 修改响应式对象的嵌套字段
queryForm.timeRange.createTimeBegin = '2025-01-01';

// 原模板的嵌套字段也被改了!(这是浅拷贝的坑)
console.log(queryFormState.timeRange.createTimeBegin); // 输出:'2025-01-01'

3.3 后续重置模板的时候,模板不干净,会有问题。

1
2
3
4
5
6
7
// 尝试重置:把模板赋值给响应式对象
Object.assign(queryForm, queryFormState);

// 第一层字段(pageNum)能重置
console.log(queryForm.pageNum); // 输出 1(正常)
// 嵌套字段(createTimeBegin)没重置!
console.log(queryForm.timeRange.createTimeBegin); // 还是 '2025-01-01'(异常)

原因:Object.assign 也是浅拷贝,只会替换第一层字段的 “值”,而 timeRange 是引用类型,模板里的 timeRange 和响应式对象里的 timeRange 是同一个对象,所以嵌套属性不会被覆盖。

示例代码中都是单层字段,暂时无问题,

1
2
3
4
5
6
7
8
// 深拷贝生成响应式对象(避免浅拷贝问题)
const queryForm = reactive(JSON.parse(JSON.stringify(queryFormState)));

// 重置表单的方法(关键:基于原始状态深拷贝重置)
const resetQueryForm = () => {
const rawState = JSON.parse(JSON.stringify(queryFormState));
Object.assign(queryForm, rawState);
};

toRaw 是什么?

toRaw 是 Vue 3 提供的 API,作用是获取响应式对象的 “原始普通对象”(解除响应式代理)。

  • 语法:toRaw(响应式对象) → 返回该对象的原始普通版本;
  • 核心价值:避免操作响应式对象时的 “代理干扰”,尤其在深拷贝 / 数据传递时,用原始对象更安全。
  • 通过 toRaw 获取的原始对象和响应式对象是 “同一个本体” —— 修改原始对象的属性,响应式对象会自动同步

封装的重置方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 通用表单重置方法
* @param {Object} reactiveForm - 响应式表单对象
* @param {Object} initialState - 初始模板对象
*/
const resetReactiveForm = (reactiveForm, initialState) => {
const rawForm = toRaw(reactiveForm);
const cleanInitial = JSON.parse(JSON.stringify(initialState));
Object.keys(rawForm).forEach(key => delete rawForm[key]);
Object.assign(rawForm, cleanInitial);
};

// 调用:一行代码重置任意表单
resetReactiveForm(queryForm, queryFormState);

// 错误写法:直接给响应式变量赋值新对象
const resetWrong = () => {
const cleanInitial = JSON.parse(JSON.stringify(queryFormState));
// ❌ 这会让queryForm失去响应式(代理被替换成普通对象)
queryForm = cleanInitial;
};

reactive 数组直接赋值会丢失响应式,需要用 Array.splice 等方法;

比较麻烦,而ref可以直接.value进行赋值,相对简单一些。

1.2 ref

在模板中组件中声明ref="alarmFormDrawer" ,在脚本里 const alarmFormDrawer = ref()变量名完全一致时,Vue 会自动把该组件的实例对象绑定到这个 ref 变量上,可以通过 alarmFormDrawer.value 直接操作这个组件。

2. APIS

2.1 axios拦截器

1
2
3
4
5
   const res = response.data;
if (res.code && res.code !== 1) {
console.log(true)
}
// 如果res.code为0,在js中代表false,就不会进这个方法中

3. table

3.1 table columes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  const tableColumns = ref([
{
title: `modelName`,
dataIndex: 'modelName',
width: 100,
ellipsis: true,
},
{
title: 'name',
dataIndex: 'name',
width: 60,
ellipsis: true,
}])
单引号':字符串字面量,最基础的字符串定义方式,无特殊功能,内容完全原样输出,适用于普通字符串(无变量 / 换行 / 特殊格式)
反引号`:模板字符串(ES6+),支持变量插值 ${}、多行字符串、特殊字符转义更灵活,适用于包含变量、换行、复杂格式的字符串

dataIndex高阶用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
1. 匹配嵌套对象的字段
如果数据源是嵌套对象,dataIndex 可以用数组指定嵌套路径:
// 嵌套数据源
const tableData = ref([
{
warnCode: 'E1001',
device: { // 嵌套对象
modelName: '温度传感器',
info: { name: '传感器1' }
}
}
]);

// 列配置匹配嵌套字段
const tableColumns = ref([
{
title: '设备型号',
dataIndex: ['device', 'modelName'], // 匹配 device.modelName
width: 100
},
{
title: '设备名称',
dataIndex: ['device', 'info', 'name'], // 匹配 device.info.name
width: 100
}
]);
2. 和自定义渲染(customRender)配合
如果需要对数据做格式化(比如错误码加颜色、时间格式化),dataIndex 先拿到原始值,再用 customRender 处理:
{
title: `错误码`,
dataIndex: 'warnCode',
width: 100,
// 自定义渲染:给错误码加红色
customRender: ({ text }) => {
// text 就是 dataIndex 匹配到的 warnCode 值(比如 E1001)
return <span style="color: red">{text}</span>;
}
}

3.2 插槽

bodyCell:个性化单元格 v-slot:bodyCell=”{text, record, index, column}”

#bodyCell 是 AntD Vue 表格组件暴露的单元格自定义插槽(具名作用域插槽),作用是:自定义表格中「指定列」的单元格渲染内容(比如把纯文本改成链接、按钮、标签等)。

它会向父组件传递 3 个核心参数(你可以理解为 “表格给插槽的数据源”):

参数 含义
column 当前单元格对应的列配置(比如 column.dataIndex 是列的 “数据索引”,对应 tableColumns 里的 dataIndex
record 当前单元格所在行的完整数据(比如 record.noticeId 是当前行的公告 ID)
text 当前单元格的原始文本值(即 record[column.dataIndex] 的值,比如列的 dataIndextitle,则 text = record.title

4. button

1
2
3
4
5
6
7
<a-button type="primary" >
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
有一个具名插槽

2.插槽

2.1 默认插槽-匿名插槽

组件内部只留一个「通用占位符」,使用者直接在组件标签内写内容即可,无需指定插槽名。

1
2
3
4
5
开发者
<button class="ant-btn">
<!-- 预留默认插槽:使用者写的内容会插到这里 -->
<slot></slot>
</button>
1
2
使用者
<a-button type="primary">查询</a-button>

2.2 具名插槽

组件内部有多个占位符:

1
2
3
4
5
6
7
8
开发者
<!-- a-button.vue -->
<button class="ant-btn">
<!-- 具名插槽:专门放图标的位置 -->
<slot name="icon"></slot>
<!-- 默认插槽:放文字的位置 -->
<slot></slot>
</button>
1
2
3
4
5
6
7
8
9
10
使用者
<!-- 你的页面 -->
<a-button type="primary">
<!-- 具名插槽:icon → 插入到组件的 <slot name="icon"> 位置 -->
<template #icon>
<SearchOutlined />
</template>
<!-- 默认插槽:无命名 → 插入到组件的 <slot> 位置 -->
查询
</a-button>

2.3 作用域插槽

组件内部的数据,能被插槽内容访问和自定义渲染

  • 普通插槽(默认 / 具名):只能从「父组件向子组件传内容」,子组件的数据无法反向给插槽用;
  • 作用域插槽:子组件把内部数据「传递给插槽」,父组件在插槽中接收并自定义渲染逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
开发者
<!-- list-component.vue -->
<div class="list">
<div v-for="item in listData" :key="item.id">
<!-- 作用域插槽:把 item 数据传递给使用者 -->
<slot name="item" :data="item"></slot>
</div>
</div>

<script setup>
const listData = [{ id: 1, name: "张三" }, { id: 2, name: "李四" }];
</script>
1
2
3
4
5
6
7
8
9
调用者
<!-- 你的页面 -->
<list-component>
<!-- 接收作用域插槽的参数:data -->
<template #item="{ data }">
<!-- 自定义渲染:比如给名字加颜色 -->
<span style="color: red">{{ data.name }}</span>
</template>
</list-component>

插槽使用小技巧

  1. 开发者可以在插槽内设置默认值。

    1
    2
    3
    4
    5
    <!-- a-button.vue 内部 -->
    <slot name="icon">
    <!-- 默认图标:使用者没传 #icon 时显示 -->
    <DefaultIcon />
    </slot>
  2. 解构插槽参数

1
2
3
<template #item="{ data: user }">
{{ user.name }} <!-- 把 data 重命名为 user,更语义化 -->
</template>

2.3 插槽的使用:

插槽的写法看似多,核心就3 类核心场景

插槽类型 核心特点 典型写法
具名插槽 有名字,精准插位置 <template #icon>
作用域插槽 子传数据,父自定义渲染 <template #item="{ user }">
默认插槽 无名字,通用占位符 <template #default="{ user }">

2.3.1 具名插槽-无参数:只塞内容,没有数据

#icon = v-slot:icon(简写),icon 是组件定义的插槽名;

1
2
3
4
5
6
<a-button>
<template #icon> <!-- 具名插槽:往按钮的icon位置插图标 -->
<SearchOutlined />
</template>
查询
</a-button>

2.3.2 作用域插槽:具名+接收参数

写法 本质 适用场景
<template #item="slotProps"> 接收完整参数对象 参数多,需要保留所有数据
<template #item="{ user }"> 解构参数对象,提取需要的字段 只需要部分参数(推荐)
<template #item="{ data: user }"> 解构 + 重命名参数 参数名不语义化,重命名

原始写法:<template #item="slotProps">:把「子组件传递的所有数据」打包成 slotProps 对象;

slotProps 这个参数不固定,写成abc都可以

1
2
3
4
5
<UserList>
<template #item="slotProps">
{{ slotProps.user.name }} - {{ slotProps.index }}
</template>
</UserList>

简化写法:<template #item="{ user }">:解构 slotProps,直接提取 user 字段,少写一层嵌套;

1
2
3
4
5
<UserList>
<template #item="{ user }">
{{ user.name }} <!-- 直接用user,不用slotProps.user -->
</template>
</UserList>

重命名写法:<template #item="{ data: user }">:子组件传递的参数名是 data,但你觉得 user 更语义化,就重命名,解决参数名不直观、和父组件变量冲突的问题。

1
2
3
4
5
6
7
8
9
<!-- 子组件传递的参数名是data -->
<slot name="item" :data="user"></slot>

<!-- 父组件重命名为user -->
<UserList>
<template #item="{ data: user }">
{{ user.name }} <!-- 用user代替data,更易读 -->
</template>
</UserList>

<template #operation="{ record }">:operation是插槽名,record是结构后的参数。

2.3.3 默认作用域插槽:默认插槽+数据

<template #default="{ user }">:组件只留了一个插槽(无名字),但需要子组件传数据(比如简单的列表组件)。:

#default 就是 “默认插槽” 的名字(可以省略不写);本质是「默认插槽 + 作用域插槽」的组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 完整写法:#default -->
<UserList>
<template #default="{ user }">
{{ user.name }}
</template>
</UserList>

<!-- 简写:省略#default,直接写template -->
<UserList>
<template v-slot="{ user }">
{{ user.name }}
</template>
</UserList>

2.4 快速判断用哪种写法的步骤

  1. 第一步:看组件有没有给插槽命名
    • 有名字(比如 icon/item/operation)→ 用 #名字
    • 没名字 → 用 #default(或省略)。
  2. 第二步:看是否需要子组件传数据
    • 不需要 → 直接写 <template #名字>内容</template>
    • 需要 → 加参数:#名字="参数对象"(或解构 #名字="{ 字段 }")。

3.组件数据传递

3.1 defineExpose

defineExpose 是 Vue 3 <script setup> 语法中专门用来向外暴露子组件内部方法 / 属性的 API,核心作用是:让父组件能通过 ref 访问子组件里的方法 / 数据(如果不写 defineExpose,父组件拿不到子组件的任何内部内容)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 子组件:AlarmFormDrawer.vue -->
<script setup>
import { ref } from 'vue';

// 子组件内部的方法(控制弹窗显示)
const showModal = () => {
const visible = ref(false);
visible.value = true;
console.log('弹窗打开了');
};

// 关键:把 showModal 方法暴露给父组件
defineExpose({
showModal // 暴露的方法名(可自定义,和内部方法名一致即可)
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 父组件 -->
<template>
<!-- 子组件标签,绑定 ref -->
<AlarmFormDrawer ref="alarmFormDrawer" />
<button @click="callChildMethod">打开子组件弹窗</button>
</template>

<script setup>
import { ref } from 'vue';
import AlarmFormDrawer from './AlarmFormDrawer.vue';

// 绑定子组件的 ref(和模板中 ref 名一致)
const alarmFormDrawer = ref();

// 父组件调用子组件暴露的方法
const callChildMethod = () => {
// 通过 .value 访问子组件暴露的 showModal 方法
alarmFormDrawer.value.showModal();
};
</script>

核心作用拆解

场景 defineExpose defineExpose
父组件通过 ref 访问子组件 alarmFormDrawer.value 是空对象(拿不到任何方法 / 属性) alarmFormDrawer.value 能拿到暴露的 showModal 方法
子组件内部逻辑 仅自己可用,对外封闭 对外开放指定方法

defineExpose 的核心价值是:打破 <script setup> 的封装性,按需向父组件开放子组件的内部能力

  • 子组件:用 defineExpose 列出要 “对外公开” 的方法 / 属性;
  • 父组件:通过 ref 拿到子组件实例后,用 .value.暴露的名称 调用 / 访问;
  • 没有它,父组件无法通过 ref 操作子组件的任何内部逻辑(这是 Vue 3 为了组件封装性做的设计)。

9. js

9.1 json/map

在 JavaScript 中,JSON(本质是字符串格式)和 Map(ES6 新增的键值对数据结构)是完全不同的概念。

维度 JSON(JSON 字符串) Map(ES6 键值对集合)
本质 文本格式的字符串(用于数据传输 / 存储) 内存中的键值对数据结构(用于代码内数据处理)
键类型限制 键必须是字符串(且需双引号包裹) 键支持任意类型(字符串、数字、对象、布尔等)
值类型限制 仅支持字符串、数字、布尔、数组、普通对象、null(不支持函数、undefined) 支持任意 JS 类型(函数、对象、undefined 等)
可直接修改? 否(需先解析为 JS 对象) 是(原生方法直接增删改查)

9.1.1 初始化方法

JSON

① 手动编写 JSON 字符串(严格格式);

1
const jsonStr = '{"name":"张三","age":25,"hobbies":["篮球","游戏"]}';

② JS 对象 / 数组 → JSON 字符串:JSON.stringify()

1
2
const userObj = {name: "张三", age: 25};
const jsonStr2 = JSON.stringify (userObj); // 输出:{"name":"张三","age":25}

MAP

① 空 Map:new Map()

1
const map1 = new Map ();

② 键值对数组初始化:new Map([[key, value], ...])

1
2
3
4
5
6
const map2 = new Map ([
["name", "张三"],
[123, "数字键"],
[{ id: 1 }, "对象键"] // JSON 无法实现
]);
//  {'name' => '张三', 123 => '数字键', {…} => '对象键'}

③ JS 对象转 Map:new Map(Object.entries(对象))

1
2
const userObj = {name: "张三", age: 25};
const map3 = new Map (Object.entries (userObj));

9.1.2. 读取/查询

操作 JSON 操作方法 Map 操作方法
语法/方法 ① JSON 字符串 → JS 对象:JSON.parse()
② 按 JS 对象方式读取(. / []
① 读取指定键:map.get(键)
② 判断键是否存在:map.has(键)
③ 获取长度:map.size
示例代码 const jsonStr = ‘{“name”:”张三”,”age”:25}’;
const userObj = JSON.parse(jsonStr);//必选
console.log(userObj.name); // 张三(点语法)
console.log(userObj[“age”]); // 25(方括号语法)
console.log(userObj.gender); // undefined(不存在的键)
const map = new Map([[“name”, “张三”], [123, “数字键”]]);
console.log(map.get(“name”)); // 张三
console.log(map.get(123)); // 数字键
console.log(map.get(“gender”)); // undefined(不存在的键)
console.log(map.has(“name”)); //
trueconsole.log(map.size); // 2
新增/修改 let jsonStr = ‘{“name”:”张三”,”age”:25}’;
// 1. 解析为对象
let userObj = JSON.parse(jsonStr);

// 2. 修改/新增属性
userObj.age = 26; // 修改已有键
userObj.gender = “男”; // 新增键

// 3. 重新转为 JSON 字符串(必选步骤)
jsonStr = JSON.stringify(userObj);
console.log(jsonStr); // {“name”:”张三”,”age”:26,”gender”:”男”}
const map = new Map([[“name”, “张三”]]);

// 修改已有键
map.set(“name”, “李四”);
// 新增数字键
map.set(123, “数字值”);
// 新增对象键
map.set({ id: 1 }, “对象值”);

console.log(map.get(“name”)); // 李四
console.log(map.size); // 3
删除 let jsonStr = ‘{“name”:”张三”,”age”:25}’;
let userObj = JSON.parse(jsonStr);

// 1. 删除对象属性
delete userObj.age;

// 2. 重新转 JSON
jsonStr = JSON.stringify(userObj);
console.log(jsonStr); // {“name”:”张三”}
const map = new Map([[“name”, “张三”], [123, “数字键”]]);

// 1. 删除指定键
map.delete(123); // 返回 true(删除成功)
map.delete(“gender”); // 返回 false(键不存在)

// 2. 清空所有键值对
map.clear();
console.log(map.size); // 0
遍历 const jsonStr = ‘{“name”:”张三”,”age”:25}’;
const userObj = JSON.parse(jsonStr);

// 1. 获取所有键
const keys = Object.keys(userObj); // [“name”, “age”]

// 2. 获取所有值
const values = Object.values(userObj); // [“张三”, 25]

// 3. 遍历键值对
Object.entries(userObj).forEach(([key, value]) => {
console.log(key, value); // name 张三;age 25
});
const map = new Map([[“name”, “张三”], [123, “数字键”]]);

// 1. 获取所有键(转数组)
const keys = […map.keys()]; // [“name”, 123]

// 2. 获取所有值(转数组)
const values = […map.values()]; // [“张三”, “数字键”]

// 3. for…of 遍历
for (const [key, value] of map) {
console.log(key, value);
}

// 4. forEach 遍历
map.forEach((value, key) => {
console.log(key, value); // 注意:回调参数是 (value, key)
});
试用场景 接口数据传输(前后端/跨端)
本地存储(localStorage)

9.2 undefind/null区别

在 JavaScript 中,undefinednull 都表示 “空值”,但设计初衷、语义、使用场景完全不同,声明 / 赋值方式也有明确规范,下面用「核心区别 + 声明方式 + 实战场景」讲透:

核心区别

维度 undefined null
语义 表示 “未定义”:变量已声明但未赋值,或属性 / 方法不存在 表示 “空值”:主动赋值为 “无 / 空”,代表 “有定义但值为空”
类型(typeof) typeof undefined → "undefined" typeof null → "object"(历史 bug,本质是原始值)
出现方式 1. 变量声明未赋值;2. 函数无返回值;3. 访问不存在的对象属性 / 数组项;4. 函数参数未传递 1. 主动赋值(如 let a = null);2. 表示 “空引用”(如 DOM 查询无结果 document.getElementById('xxx') → null
相等性(==) undefined == null → true(值相等) 同上
严格相等(===) undefined === null → false(类型不同) 同上

声明/赋值方式

undefined:无需主动声明,由 JS 自动赋值

undefined 是 JS 内置的原始值,不建议手动赋值(语义混乱),通常由 JS 自动产生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 场景1:变量声明但未赋值 → 自动为 undefined
let a;
console.log(a); // undefined

// 场景2:函数无返回值 → 返回 undefined
function fn() {}
console.log(fn()); // undefined

// 场景3:访问不存在的属性 → 返回 undefined
const obj = { name: '张三' };
console.log(obj.age); // undefined

// 场景4:函数参数未传递 → 形参为 undefined
function fn2(param) {
console.log(param); // undefined
}
fn2();

// ❌ 不推荐:手动赋值 undefined(破坏语义,不如用 null)
let b = undefined;

null:必须主动赋值,代表 “显式空值”

null 是 “程序员主动标记的空”,只有手动赋值才会出现,是规范的 “空值声明方式”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 场景1:声明变量时,明确表示“初始为空”
let user = null; // 后续会赋值为用户对象,初始为空

// 场景2:释放变量引用(避免内存泄漏)
let obj = { name: '张三' };
obj = null; // 主动清空引用,让垃圾回收器回收

// 场景3:DOM 查询无结果(JS 内置返回 null)
const dom = document.getElementById('non-exist');
console.log(dom); // null

// 场景4:函数返回“无结果”(主动标记)
function getUserId() {
if (!hasLogin()) {
return null; // 明确表示“无用户ID”,而非“未定义”
}
return 123;
}
1
2
3
4
5
6
7
8
9
// 方式1:利用 ==(简洁)
if (value == null) {
// value 是 undefined 或 null
}

// 方式2:严格判断(更清晰)
if (value === undefined || value === null) {
// 处理空值
}