Vue3 v3.4之前如何实现组件中多个值的双向绑定?

Vue3 v3.4之前如何实现组件中多个值的双向绑定?

官方给的例子是关于el-input的,如下。但是@input不是所有组件标签都有的属性啊,有没有一种通用的办法呢?

language-html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
defineProps({

firstName: String,
lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>

基础代码

以一个Dialog组件为例。我们自己写一个course-buy.vue

language-html
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
<template>
<el-dialog
v-model="localValue.dialogVisible"
title="Warning"
width="500"
align-center
>
<span>Open the dialog from the center from the screen</span>
<template #footer>
<div class="dialog-footer">
<el-button @click="localValue.dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="localValue.dialogVisible = false">
Confirm
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import {
PropType} from "vue";

//对外变量
const props = defineProps({

dialogVisible: Object as PropType<boolean>,
courseId: Object as PropType<string | number>,
})
const emit = defineEmits(['update:dialogVisible','update:courseId'])
//本地变量
const localValue = reactive({

dialogVisible: props.dialogVisible,
courseId: props.courseId
})

</script>

外部在使用时(假设为base.vue),如下使用

language-html
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<CourseBuy
v-model:dialog-visible="orderPayParams.dialogVisible"
v-model:course-id="orderPayParams.courseId"
/>
</template>
<script setup lang="ts">
const orderPayParams = reactive({

dialogVisible: false,
courseId: 1
});
</script>

上述代码,course-buy.vue中真正使用的变量是localValue本地变量,localValue的值来自base.vue
但是上述的基础代码,dialogVisiblecourseId的值只能从base.vue流向course-buy.vue
如何实现course-buy.vue本身修改localValue的值后,修改变化同步到base.vue呢?

1. watch

如果要让dialogVisible双向绑定,可以写两个watch互相监听并更新。要实现courseId双向绑定也是同理。

language-html
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
<script setup lang="ts">
import {
PropType} from "vue";

//对外变量
const props = defineProps({

dialogVisible: Object as PropType<boolean>,
courseId: Object as PropType<string | number>,
})
const emit = defineEmits(['update:dialogVisible','update:courseId'])
//本地变量
const localValue = reactive({

dialogVisible: props.dialogVisible,
courseId: props.courseId
})
//值双向绑定
watch(() => props.dialogVisible, (newValue) => {

localValue.dialogVisible = newValue;
});
watch(() => localValue.dialogVisible, (newValue) => {

emit('update:dialogVisible', newValue);
});
</script>

2. computed(推荐)

不过使用computed可以更简洁,性能也更好。

language-html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup lang="ts">
import {
PropType} from "vue";

//对外变量
const props = defineProps({

dialogVisible: Object as PropType<boolean>,
courseId: Object as PropType<string | number>,
})
const emit = defineEmits(['update:dialogVisible','update:courseId'])
//本地变量
const localValue = reactive({

dialogVisible: computed({

get: () => props.dialogVisible,
set: (value) => emit('update:dialogVisible', value)
}),
courseId: props.courseId
})
</script>
Vue3炫酷商品卡牌 组件设计

Vue3炫酷商品卡牌 组件设计

感谢来自BinaryMoon-CSS 艺术之暗系魔幻卡牌的博文。💕

演示

代码

接口类型

language-typescript
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
export interface CourseBaseVO {

/**
* 主键
*/
id: string | number;

/**
* 机构ID
*/
companyId: string | number;

/**
* 课程名称
*/
name: string;

/**
* 大分类
*/
mt: string;

/**
* 小分类
*/
st: string;

/**
* 课程图片
*/
pic: string;

/**
* 是否收费
*/
charge: boolean;

/**
* 原价
*/
originalPrice: number;

/**
* 现价
*/
price: number;

/**
* 评分
*/
star: number;

/**
* UNPUBLISHED(1, "未发布"), UNDER_REVIEW(2, "审核中"), REVIEW_FAILED(3, "审核不通过"), REVIEW_PASSED(4, "审核通过")
*/
status: number;

/**
* 审核意见
*/
mind: string;

}
interface CourseBaseExtraHotVo extends CourseBaseVO {

isHot: boolean;
}

外部资源wave_orange.svg

language-txt
1
<svg width="100%" height="100%" id="svg" viewBox="0 0 1440 490" xmlns="http://www.w3.org/2000/svg" class="transition duration-300 ease-in-out delay-150"><defs><linearGradient id="gradient" x1="0%" y1="51%" x2="100%" y2="49%"><stop offset="5%" stop-color="#fcb900"></stop><stop offset="95%" stop-color="#ff6900"></stop></linearGradient></defs><path d="M 0,500 L 0,0 C 90.96650717703349,54.02870813397129 181.93301435406698,108.05741626794259 268,115 C 354.066985645933,121.94258373205741 435.23444976076553,81.79904306220095 535,84 C 634.7655502392345,86.20095693779905 753.129186602871,130.7464114832536 867,132 C 980.870813397129,133.2535885167464 1090.248803827751,91.2153110047847 1185,62 C 1279.751196172249,32.78468899521531 1359.8755980861245,16.392344497607656 1440,0 L 1440,500 L 0,500 Z" stroke="none" stroke-width="0" fill="url(#gradient)" fill-opacity="0.53" class="transition-all duration-300 ease-in-out delay-150 path-0"></path><defs><linearGradient id="gradient" x1="0%" y1="51%" x2="100%" y2="49%"><stop offset="5%" stop-color="#fcb900"></stop><stop offset="95%" stop-color="#ff6900"></stop></linearGradient></defs><path d="M 0,500 L 0,0 C 111.98086124401911,108.89952153110048 223.96172248803822,217.79904306220095 335,271 C 446.0382775119618,324.20095693779905 556.133971291866,321.7033492822967 626,309 C 695.866028708134,296.2966507177033 725.5023923444976,273.3875598086125 820,274 C 914.4976076555024,274.6124401913875 1073.8564593301435,298.7464114832536 1188,257 C 1302.1435406698565,215.25358851674642 1371.0717703349283,107.62679425837321 1440,0 L 1440,500 L 0,500 Z" stroke="none" stroke-width="0" fill="url(#gradient)" fill-opacity="1" class="transition-all duration-300 ease-in-out delay-150 path-1"></path></svg>

组件源码

language-html
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
<template>
<div
id="card"
style="padding: 5px;margin: 20px"
>
<el-card
shadow="hover"
style="width: 350px; border-radius: 10px"
>
<div class="wave-orange-card"></div>
<div style="display: flex;flex-direction: column;justify-content: center;align-items: center;">
<el-image
:src="fileBaseUrl+courseBase.pic"
fit="fill"
style="width: 200px"
/>
</div>
<div style="height: 30px; font-size: 20px;margin-top: 10px">
<span>{
{ courseBase.name }}</span>
</div>
<div style="height: 40px; ">
<el-rate
v-model="courseBase.star"
size="large"
show-score
text-color="#ff9900"
:score-template="courseBase.star.toString() + 'points'"
disabled
/>
</div>
<div style="height: 40px; ">
<el-tag v-if="courseBase.charge" type="warning" size="large" effect="light">
<span style="font-size: 20px;font-weight: bold">¥{
{ courseBase.price }}</span>
&nbsp;&nbsp;
<span class="slash-deleted-text" style="font-size: 14px;color: #909399">{
{ courseBase.originalPrice }}</span>
</el-tag>
<el-tag v-else type="success" size="large"><span style="font-size: 20px">免费</span></el-tag>
<span>&nbsp;&nbsp; 6w人报名</span>
</div>
</el-card>
</div>
</template>

<script lang="ts" setup>

import {
CourseBaseVO} from "@/api/course/types";
import {
PropType} from "vue";

const fileBaseUrl = import.meta.env.VITE_APP_MINIO_FILE_URL;
interface CourseBaseExtraHotVo extends CourseBaseVO {

isHot: boolean;
}
const props = defineProps({

courseBase: Object as PropType<CourseBaseExtraHotVo>,
});
const emit = defineEmits(['update:courseBase'])


</script>

<style scoped>

/* 卡片图片背景 */
:deep(.wave-orange-card){

background-image: url("src/assets/svg/wave_orange.svg");
background-repeat: no-repeat;
background-size: cover; /* 或使用 100% 100% 来确保完全覆盖 */
background-position: center; /* 根据需要调整 */
overflow: hidden; /* 避免内容溢出 */
position: absolute; /* 固定定位,不随滚动条移动 */
width: 310px; /* card的宽度为350 */
height: 200px; /*pic的大小为200*200*/
opacity: 0.6;
}

/* 删除线 */
:deep(.slash-deleted-text) {

position: relative;
overflow: hidden; /* 防止斜线溢出容器 */
}

:deep(.slash-deleted-text::after) {

content: '';
position: absolute;
left: 0;
top: 10%; /* 调整为文本高度的一半 */
width: 100%; /* 与容器同宽 */
border-bottom: 1px solid #F56C6C; /* 删除线的样式 */
transform: rotate(25deg); /* 调整角度为倾斜 */
transform-origin: left bottom;
}

/* 卡片背景 */
:deep(:root) {

--margin: 100px;
/* 上演一出黄金分割率的好戏 */
--card-width: 360px;
/* 上演一出黄金分割率的好戏 */
--card-height: calc(var(--card-height) * 1.618);
}

#card{

width: var(--card-width);
height: var(--card-height);
position: relative;
cursor: pointer;
transition: transform 0.4s ease; /* 设置放大动画的过渡效果为2秒 */
}

/* 定义自定义属性 --rotate */
@property --rotate{

/* 自定义属性的默认值 */
initial-value: 90deg;
/*
定义自定义属性允许的语法结构,
此处定义该元素仅接受角度值。
*/
syntax: '<angle>';
/* 定义该自定义属性是否允许被其他元素所继承 */
inherits: false;
}

/* 定义动画 */
@keyframes edge{

from{

--rotate: 0deg;
}
to{

--rotate: 360deg;
}
}

#card::before{

content: '';
width: 104%;
height: 102%;
background: linear-gradient(var(--rotate),
rgb(44, 251, 255), rgb(81, 154, 255), rgb(97, 57, 242));
position: absolute;
z-index: -1;
top: -1%;
left: -2%;
/* 设置边框圆角半径 */
border-radius: 0.5vw;
/*
为当前元素指定使用的动画,并将该动画的
持续时间设置为 3.5s,动画速度保持不变,
动画播放次数为无限次。
*/
animation: edge 10s linear infinite;
}

#card::after{

content: '';
width: 80%;
height: 100%;
background: linear-gradient(var(--rotate),
rgb(44, 251, 255), rgb(81, 154, 255), rgb(97, 57, 242));
position: absolute;
top: 5%;
left: 10%;
filter: blur(2vw);
z-index: -1;
/* 使用动画 */
animation: edge 3.5s linear infinite;
}

/* 卡片悬浮变化背景 */
#card:hover {

transform: scale(1.02); /* 鼠标悬浮时放大到1.1倍 */
}

#card::before, #card::after {

transition: background 1s ease; /* 将过渡应用于background,确保背景渐变的平滑变化 */
}

#card:hover::before, #card:hover::after {

background: linear-gradient(var(--rotate), #f82747, #fc5c7c, #ffc3d3); /* 渐变为淡红色 */
}

</style>

使用示例

language-html
1
2
3
4
5
6
7
8
9
<template>
<CourseCard
v-for="(course, index) in hotList"
:course-base="course"
/>
</template>
<script lang="ts" setup>
const hotList = ref<CourseBaseExtraHotVo[]>([]);
</script>
Vue3 setup路由进入以及变化问题

Vue3 setup路由进入以及变化问题

1. 起因

在Vue 3中,<script setup>语法糖提供了一种更加简洁和组合式的方式来定义组件。然而,由于<script setup>的特性,它不能直接使用beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave这些导航守卫。

但是vue-router中有两个的类似函数可以触发路由离开和变化,只需要import一下就可以。

language-js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import {
onBeforeRouteLeave, onBeforeRouteUpdate } from "vue-router";

onBeforeRouteUpdate((to, from, next) => {

console.log("onBeforeRouteUpdate",to,from, typeof next)
next();
})
onBeforeRouteLeave((to, from, next)=>{

console.log("beforeRouteLeave",to,from, typeof next)
next()
})
</script>

但是却没有beforeRouteEnter的替代品。

2. 浏览过的代替方法

https://github.com/vuejs/rfcs/discussions/302#discussioncomment-2794537
https://blog.richex.cn/vue3-how-to-use-beforerouteenter-in-script-setup-syntactic-sugar.html
https://blog.csdn.net/oafzzl/article/details/125045087

但是都是在<script setup>之上新加了第二个<script>,用第二个<script>来使用无setupbeforeRouteEnter。限制很多,尤其是两个script之间互动很麻烦,甚至无法实现。

3. 还是Watch

https://juejin.cn/post/7171489778230100004
https://blog.csdn.net/m0_55986526/article/details/122827829

下面是一个当路由从"/stock/move/moveDetail""/stock/move/moveSearch"触发函数的例子。同时第一次进入时也会触发watch,就相当于beforeRouteEnter了。

language-js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import {
watch} from "vue";
import {
useRouter} from "vue-router";
let router = useRouter()
// 监听当前路由变化
watch(() => router.currentRoute.value,(newPath, oldPath) => {

if (newPath != null && oldPath != null && "fullPath" in newPath && "fullPath" in oldPath && newPath["fullPath"] === "/stock/move/moveSearch" && oldPath["fullPath"] === "/stock/move/moveDetail"){

onSubmitQuiet()
}
}, {
immediate: true});
</script>
ElementPlus表单验证v-for循环问题

ElementPlus表单验证v-for循环问题

提供两个样例,主要注意<el-form-item>中的prop

样例一

language-html
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
39
40
41
42
43
44
45
46
47
48
<template>
<el-form
ref="dynamicValidateForm"
:model="dynamicValidateForm"
label-width="120px"
class="demo-dynamic"
>
//重点关注el-form-item标签中的prop内容
<el-form-item
v-for="(domain, index) in dynamicValidateForm.domains"
:key="domain.key"
:label="'Domain' + index"
:prop="'domains.' + index + '.value'"
:rules="{
required: true,
message: 'domain can not be null',
trigger: 'blur',
}"
>
<el-input v-model="domain.value"></el-input>
</el-form-item>
</el-form>
</template>

<script lang="ts">
export default {

data() {

return {

dynamicValidateForm: {

domains: [
{

key: 1,
value: '',
},
],
email: '',
},
}
},
}
</script>


样例二

language-html
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
<template>
<el-form :model="queryParams" style="width: 100%">
<el-table :data="queryParams.items">
<el-table-column label="移动数量" width="100">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-form-item style="width: 100px;padding: 0px;margin: 0px" :prop="'items.'+scope.$index+'.moveAmount'" :rules="{ required: true, message: '不能为空', trigger: 'blur',}">
<el-input-number class="number" :controls="false" v-model="scope.row.moveAmount"></el-input-number>
</el-form-item>
</div>
</template>
</el-table-column>
</el-table>
</el-form>
</template>

<script setup>
const queryParams = reactive({

factory1: undefined,
factory2: undefined,
commentText: undefined,
items:[]
})
</script>
ElementPlus隐藏Scrollbar的横向滚动条
ElementPlus-穿梭框长宽设置

ElementPlus-穿梭框长宽设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<style scoped>
:deep(.el-transfer-panel) {
height: 400px; /* 穿梭框高度 */
width: 300px;
}

:deep(.el-transfer-panel__list.is-filterable) {
height: 400px; /* 穿梭框列表高度 */
}

:deep(.el-transfer-panel__body) {
height: 400px; /* 穿梭框视图层高度 */
}
:deep(.el-transfer-panel__filter) {
width: 300px; /* 修改搜索栏的宽度 */
}
</style>
Vue3 setup组合式语法优雅地使用Echarts库

Vue3 setup组合式语法优雅地使用Echarts库

1. 安装Echarts

npm或者yarn安装

npm install echarts
yarn add echarts

2.main.js全局挂载Echarts

language-javascript
1
2
3
4
5
6
7
8
9
import {
createApp } from 'vue'
import App from './App.vue'

import * as echarts from 'echarts'

const app = createApp(App)
app.config.globalProperties.$echarts = echarts // 全局挂载echarts
app.mount('#app')

3.Vue setup组合式语法使用案例

language-html
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<template>
<!-- 通过ref获取html元素 宽高必须设置 -->
<el-row>
<el-col :offset="8" :span="8">
<div ref="info" style="width: 100%; height: 600px"></div>
</el-col>
</el-row>
</template>
<script setup>
import {
onMounted, ref, inject } from "vue";
const {
proxy } = getCurrentInstance()//获取Vue3全局配置
const info = ref();//用来获取对应标签组件

onMounted(() => {

var infoEl = info.value;//获取ref="info"的标签组件
var userEc = proxy.$echarts.init(infoEl, "light");//proxy.$echarts是在获取全局配置中的echarts,这样就不需要在每个vue中import echarts了

//此处为图表配置区,可参考Echarts官网替换各种图表
var option = {

tooltip: {

trigger: 'item'
},
legend: {

top: '5%',
left: 'center'
},
series: [
{

name: 'Access From',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {

borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {

show: false,
position: 'center'
},
emphasis: {

label: {

show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {

show: false
},
data: [
{
value: 1048, name: 'Search Engine' },
{
value: 735, name: 'Direct' },
{
value: 580, name: 'Email' },
{
value: 484, name: 'Union Ads' },
{
value: 300, name: 'Video Ads' }
]
}
]
};
userEc.setOption(option);//将图标挂载到标签组件
});
</script>

(完)

Vue-Router-编程式路由跳转

Vue-Router-编程式路由跳转

历届办法

  1. path+query
    参数会暴露在url
  2. name+params
    官方在2022–8-22已禁用params传参,具体看这This
  3. state
    参数不会暴露在url,但刷新页面会失效,和以前的params一样
  4. store
    用额外的插件来store,顾名思义存储数据,此本章不做讲解。

起手式:配置路由

无论是path还是name,都需要在路由配置中指定每个路径对应的组件。将配置都写到一个配置文件中,然后在vue的main.js中挂载配置它,具体流程是这样的:

  1. 首先,您需要在项目的src目录下创建一个router文件夹,用来存放路由相关的文件。
  2. 然后,在router文件夹中创建一个index.js文件,用来编写路由配置。例如:
language-js
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
// router/index.js
import {
createRouter, createWebHashHistory } from 'vue-router'

// 导入路由组件
import Home from '.../components/Home.vue'
import About from '.../components/About.vue'

// 定义路由
//path
const routes = [
{
path: '/', component: Home },
{
path: '/about', component: About },
]
/*
//name
const routes = [
{ name: "Home", path: "/", component: Home }
{ name: "About ", path: "/about", component: About }
]

*/
// 创建路由实例
const router = createRouter({
history: createWebHashHistory(), routes, })

// 导出路由实例
export default router
  1. 最后,在main.js文件中导入路由器对象,并将其挂载到Vue实例上。例如:
language-js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// main.js
// 在main.js中
import {
createApp } from 'vue'
import App from './App.vue'

// 导入路由实例
import router from './router'

// 创建并挂载根实例
const app = createApp(App)
// 使用路由实例
app.use(router)
app.mount('#app')

1. path+query

发送方

language-typescript
1
2
3
4
5
6
7
8
9
10
11
<script setup lang="ts">
import {
useRouter } from "vue-router";
const router = useRouter();
const goPath = (id: number) => {

router.push({
path: "/about", query: {
id: id } });
};
</script>

接收方

language-typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts">
import {
useRoute } from "vue-router";
const route = useRoute();
const id = route.query.id;
</script>

<template>
<div>Id: {
{
id }}</div>
</template>

2. state

发送方

language-typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup lang="ts">
import {
useRouter } from "vue-router";
const router = useRouter();
const goPath = (id: number) => {

router.push({
path: "/about", state: {
id: id } });
//或者
//router.push({ name: "About", state: { id: id } });
};
</script>

接收方

language-typescript
1
2
3
4
5
6
7
8
9
10
<script setup lang="ts">
const id = history.state.data;
</script>

<template>
<div>Id: {
{
id }}</div>
</template>

Vue-ts-优雅的响应式对象数组(reactive型数组)

Vue-ts-优雅的响应式对象数组(reactive型数组)

需求

绘制如下画面,每一行绑定的数据都是独立的,并且还需要完成”增加行”按钮。

Vue前端代码

language-html
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
39
40
41
42
43
44
45
46
47
48
<el-button @click="addLine">增加行</el-button>
<el-row>
<el-col :span="4" align="middle">产品</el-col>
<el-col :span="4" align="middle">仓库</el-col>
<el-col :span="4" align="middle">批次号</el-col>
<el-col :span="4" align="middle">库存数量</el-col>
<el-col :span="4" align="middle">隔离数量</el-col>
</el-row>
<el-form>
<el-row v-for="(item, i) in inItemNums">
<el-col :span="4">
<el-select v-model="inQueryParams[i].product" placeholder="请选择产品" @change="inSelectProductChange(i)">
<el-option
v-for="item in inSelectOption[i].product"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col :span="4">
<el-select v-model="inQueryParams[i].factory" placeholder="Select" @change="inSelectFactoryChange(i)">
<el-option
v-for="item in inSelectOption[i].factory"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col :span="4">
<el-select v-model="inQueryParams[i].batchNum" placeholder="Select" @change="inSelectBatchNumChange(i)">
<el-option
v-for="item in inSelectOption[i].batchNum"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col :span="4">
<el-input v-model="inQueryParams[i].stockQuantity" readonly></el-input>
</el-col>
<el-col :span="4">
<el-input v-model="inQueryParams[i].isolateQuantity"></el-input>
</el-col>
</el-row>
</el-form>

Vue逻辑实现

language-typescript
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
39
40
41
42
43
44
45
46
47
48
49
50
51
interface IselectOptionItem {

product: [],
factory: [],
batchNum: [],
isolateQuantity: []
}
const selectOptionItem = {

product: [],
factory: [],
batchNum: [],
isolateQuantity: []
}
interface IqueryParamsItem {

product: undefined,
factory: undefined,
batchNum: undefined,
stockQuantity: undefined,
isolateQuantity: undefined
}
const queryParamsItem = {

product: undefined,
factory: undefined,
batchNum: undefined,
stockQuantity: undefined,
isolateQuantity: undefined
}
const inItemNums = ref(5);
const inSelectOption = reactive<IselectOptionItem []>([])
for (let i = 0; i < inItemNums.value; i++) {

inSelectOption.push({
...selectOptionItem})
}
const inQueryParams = reactive<IqueryParamsItem[]>([]);
for (let i = 0; i < inItemNums.value; i++) {

inQueryParams.push({
...queryParamsItem})
}
const addLine=()=>{

inSelectOption.push({
...selectOptionItem})
inQueryParams.push({
...queryParamsItem})
inItemNums.value = inItemNums.value+1;
}

注意点

往数组添加元素时,不使用selectOptionItem而是{...selectOptionItem},区别是前者是引用,而后置是复制,如果写前者,那么数组里的元素都指向一个对象,那么就会出现下拉框的值一起联动。