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>
SpringBoot后端Long数据传到前端js精度损失问题

SpringBoot后端Long数据传到前端js精度损失问题

方案一、修改后端

在对应的字段上添加注解,将Long转为String后传输。

language-java
1
2
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long payNo;

方案二、修改前端

js对应的结果接收上使用BigInt

language-js
1
2
3
4
5
6
xxx().then((res) => {

if(res){

this.payNo = String(BigInt(res.payNo))
}
JavaWeb el-upload图片上传SpringBoot保存并且前端使用img src直接访问(基于RuoYi-Vue3)

JavaWeb el-upload图片上传SpringBoot保存并且前端使用img src直接访问(基于RuoYi-Vue3)

一、Vue前端

language-html
1
2
3
4
5
6
7
8
9
10
11
12
<el-upload
v-model:file-list="createParams.fileList"
action="http://localhost:8080/DataMgt/uploadPic"
:headers="{ Authorization: 'Bearer ' + getToken() }"
list-type="picture-card"
:on-success="handleSuccess"
:before-upload="handlePicBeforeUpload"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
>
<el-icon><Plus /></el-icon>
</el-upload>

需要注意的是action填写后端api路径,同时header需要填写token不然会被拦截,其余的没有影响可以翻阅文档理解。

二、SpringBoot后端

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/DataMgt")
public class Home extends BaseController {

private static String picDir = "C:\\Users\\mumu\\Desktop\\images\\";

@RequestMapping("/uploadPic")
public AjaxResult uploadPic(MultipartFile file) throws IOException {

String filePath = picDir+ UUID.randomUUID().toString() + file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf('.'));
File saveF = new File(filePath);
logger.info("save pic:"+filePath);
file.transferTo(saveF);
return success(filePath);
}
}

picDir填写的是图片保存到哪个文件夹路径,无论你是windows开发环境还是linux生产环境,这个路径都应该是绝对路径。
现在图片保存到了这个路径,那么我们前端imgsrc又如何才能经由后端直接访问图片呢?

第一,需要通过SpringBoot的@Configuration配置一个映射,如下

language-java
1
2
3
4
5
6
7
8
9
10
@Configuration
public class StaticConfig implements WebMvcConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {

registry.addResourceHandler("/images/**")
.addResourceLocations("file:C:\\Users\\mumu\\Desktop\\images\\");
}
}

意思是将C:\\Users\\mumu\\Desktop\\images\\路径映射到/images/,那么当你访问http://localhost:8080/images/1.png时其实就是在访问C:\\Users\\mumu\\Desktop\\images\\1.png

第二,需要在若依的com.ruoyi.framework.config.SecurityConfigconfigure函数中中配置访问权限,将/images/添加其中,对所有人开放,如下

language-java
1
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**", "/images/**").permitAll()

最后,在前端,像这样就可以经由后端访问图片啦。

language-html
1
<el-image src="http://localhost:8080/images/1.png" style="width: 200px;height: 200px"></el-image>

三、延伸

这是多图片上传组件,但其实每张都会和后端交互一次。

如果这个图片墙和文字搭配在一起成为一个图文动态发布页面,那么创建主副数据表,主表保存idtext表示id和文字,副表保存master_idname,表示主表id,图片名。
当新建动态时,在前端即刻生成当前时间作为图文动态的唯一标识。
当上传图片时,以唯一标识作为master_id并以保存的名称作为name插入副表。
当动态提交时,以唯一标识作为id并以文本内容作为text插入主表。
当取消时,以唯一标识为查询条件删除主副表相关数据和图片资源。

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>
JavaWeb Springboot后端接收前端Date变量

JavaWeb Springboot后端接收前端Date变量

如果前端传输的日期符合ISO 8601格式,那么后端用@PostMapping + @RequestBody那么springboot就会自动解析。

如果传输的日期格式不符合ISO 8601格式或者传输用的是Get方法,那么在接收传输的实体类中加上@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss"),这里面的pattern对应传来的日期格式。

language-html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ISO 8601是一种国际标准的日期和时间表示方式。这种格式的主要特点包括:

- 时间日期按照年月日时分秒的顺序排列,大时间单位在小时间单位之前。
- 每个时间单位的位数固定,不足时于左补0。
- 提供两种方法来表示时间:其一为只有数字的基础格式;其二为添加分隔符的扩展格式,让人能更容易阅读。

具体来说,ISO 8601的日期和时间表示方式的格式为`YYYY-MM-DDTHH:mm:ss.sssZ`,其中:

- `YYYY`代表四位数年份。
- `MM`代表月份。
- `DD`代表天数。
- `T`作为日期和时间的分隔符。
- `HH`代表小时。
- `mm`代表分钟。
- `ss.sss`代表秒和毫秒。
- `Z`代表的是时区。

例如,

"2023年11月15日06时16分50秒"可以表示为"2023-11-15T06:16:50"。
"2023-11-15"是ISO 8601日期格式的一个子集。在ISO 8601中,日期可以表示为"YYYY-MM-DD"的格式。因此,"2023-11-15"是符合ISO 8601日期格式的。

JavaWeb后端解析前端传输的excel文件(SpringBoot+Vue3)

JavaWeb后端解析前端传输的excel文件(SpringBoot+Vue3)

一、前端

前端拿Vue3+ElementPlus做上传示例

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
<template>
<el-button type="primary" @click="updateData" size="small" plain>数据上传</el-button>
<el-dialog :title="upload.title" v-model="upload.open" width="400px" align-center append-to-body>
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<el-icon class="el-icon--upload">
<upload-filled/>
</el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<span>仅允许导入xls、xlsx格式文件。</span>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitFileForm">确 定</el-button>
<el-button @click="upload.open = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>

const upload = reactive({

// 是否显示弹出层(用户导入)
open: false,
// 弹出层标题(用户导入)
title: "标题",
// 是否禁用上传
isUploading: false,
// 是否更新已经存在的用户数据
updateSupport: 0,
// 设置上传的请求头部
headers: {
Authorization: "Bearer " + getToken()},
// 上传的地址
url: import.meta.env.VITE_APP_BASE_API + "/yourControllerApi"
});

//数据上传
const updateData = () => {

upload.title = "数据上传";
upload.open = true;
}

//文件上传中处理
const handleFileUploadProgress = (event, file, fileList) => {

upload.isUploading = true;
console.log(upload.url)
};

//文件上传成功处理
const handleFileSuccess = (rp, file, fileList) => {

//这里的rp就是后端controller的响应数据
console.log(rp)
upload.open = false;
upload.isUploading = false;
proxy.$refs["uploadRef"].handleRemove(file);
};
</script>

二、后端

  1. 导入依赖
language-xml
1
2
3
4
5
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
  1. controller接收file
language-java
1
2
3
4
5
@PostMapping("/yourControllerApi")
public AjaxResult importData(@RequestBody MultipartFile file){

return stockMgntService.importData(file);
}
  1. service具体实现
language-java
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
@Service
public class StockMgntServiceImpl implements StockMgntService {

@Override
public AjaxResult importData(MultipartFile file) {

try(InputStream is = file.getInputStream();) {

Workbook workbook = WorkbookFactory.create(is);
int numberOfSheets = workbook.getNumberOfSheets();
if (numberOfSheets != 1){

//要处理多个sheet,循环就可以
System.out.println("只允许有1个Sheet");
return null;
}
//取得第一个sheet
Sheet sheet = workbook.getSheetAt(0);
//获取最后一行的索引,但通常会比预想的要多出几行。
int lastRowNum = sheet.getLastRowNum();
//循环读取每一行
for (int rowNum = 0; rowNum < lastRowNum; rowNum++) {

Row rowData = sheet.getRow(rowNum);
if (rowData != null){

//可以使用rowData.getLastCellNum()获取最后一列的索引,这里只有6行,是假设现在的excel模板是固定的6行
for(int cellNum = 0; cellNum < 6; ++cellNum){

Cell cell = rowData.getCell(cellNum);
if (cell == null){
continue;}
cell.setCellType(CellType.STRING);
if (!cell.getStringCellValue().isEmpty()){

System.out.println(cell.getStringCellValue());
}
/*
利用这个方法可以更好的将所有数据以String获取到
Cell cell = productCodeRow.getCell(cellNum , Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
String cellValue = dataFormatter.formatCellValue(cell );
*/
}
}
}
}catch (Exception e) {

System.out.println(e.getMessage());
throw new RuntimeException();
}
}
}

三、结语

这样的方式只能处理简单的excel,对于结构更复杂的excel,需要其他工具utils结合。
以及可以结合SpringBoot有更好的poi使用方法。

JavaWeb后端将excel文件传输到前端浏览器下载(SpringBoot+Vue3)

JavaWeb后端将excel文件传输到前端浏览器下载(SpringBoot+Vue3)

一、后端

  1. 导入依赖
language-xml
1
2
3
4
5
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
  1. controller层
    其中@RequestParam String moveNo是前端传入的参数,而HttpServletResponse response不是,前者可以没有,后者一定要有。
language-java
1
2
3
4
5
6
7
@GetMapping("/yourControllerApi")
public AjaxResult exportData(@RequestParam String moveNo, HttpServletResponse response){

System.out.println(moveNo);
System.out.println(response);
return stockMgntService.exportData(moveNo, response);
}
  1. service层
language-java
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
@Service
public class StockMgntServiceImpl implements StockMgntService {

@Override
public AjaxResult exportData(String moveNo, HttpServletResponse response) {

//这里是从后台resources文件夹下读取一个excel模板
ClassPathResource resource = new ClassPathResource("template/模板.xlsx");
try(InputStream fis = resource.getInputStream();
Workbook workbook = WorkbookFactory.create(fis);
ServletOutputStream os = response.getOutputStream()
){

//这块写入你的数据
//往第一个sheet的第一行的第2列和第5列写入数据
Sheet sheet = workbook.getSheetAt(0);
sheet.getRow(0).getCell(1, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).setCellValue("test");
sheet.getRow(0).getCell(5, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).setCellValue("test");
/*......你的操作......*/
//这块开始配置传输
response.setCharacterEncoding("UTF-8");
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-disposition", "attachment;filename=template.xlsx");
response.flushBuffer();
workbook.write(os);
os.flush();
os.close();
}catch (IOException e) {

throw new RuntimeException(e);
}
return null;
}
}

二、前端

axios向对应api发送请求并携带参数,然后使用Blob来接收后端的OutputStream输出流并保存下载保存到本地浏览器。

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
<template>
<el-button type="primary" @click="downloadData" size="small" plain>模板下载</el-button>
</template>

<script setup>
const downloadData = () => {

console.log(queryParams.moveNo)
axios({

url: '/yourControllerApi',
method: 'get',
params: {
moveNo:'0000212132142'},
responseType: "blob"
}).then(rp=>{

console.log(rp)
const blob = new Blob([rp], {
type: 'application/vnd.ms-excel'});
let link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.setAttribute('download', '模板下载.xlsx');
link.click();
link = null;
});
}
</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>