太忙了,偶尔也搞搞前端,德智体美全方位发展,但是不能忘记自己最爱的还是Java哈哈哈
多亏:
uniapp插件市场
需求概述:
提交一条故障核实的记录数据,其中包括:
①是否准确(单选)
②故障描述(多行文本300字以内)
③故障照片(3张以内,单张不超过10M)
业务逻辑:
对于照片的选择机制,基本在发朋友圈模式上进行了一些改动:选择照片按钮和照片一样大,选完后照片出现在原来按钮的位置,按钮移动到下一张照片位置,当选完第三张时,选择按钮图片消失,并提示最多选择3张,如图所示:
选择照片并显示以后,照片暂存在本地,并不上传至服务器,直到用户点击“故障确认”按钮,才将图片上传至服务器。
每张照片右上角有1/4圆的删除图片按钮,点击可以删除图片
进到这个页面有两种情况,
第一:数据库中没有此条记录的数据,则为add状态,可以正常选择后提交到后台执行insert语句
第二:数据库中有此条记录的数据,则为edit状态,但是先禁用页面所有可输入的按钮,若想改变数据内容,点击右上角编辑按钮,即可编辑。
点击故障核实提交后,先执行上传图片的request,然后把图片路径返回给前端,再执行入库的request,这就要求第一个request必须是同步的,由于是用的uni.uploadFile,没找到它自己设置同步的办法,所以用了一个定时器来阻塞,这个还是比较巧妙的值得借鉴的。
源代码
主页面
<template>
<view>
<view class="cell" >
<view class="cell-left">
<view class="cell-text">是否准确:</view>
<view class="cell-text">
<view class="radio-content">
<view class="radio-content" @click="changeRadio1">
<text style="color: black;">是</text>
<view class="radio" :class="radio1 == 1 ? 'radio-default':''">
<view :class="radio1 == 1 ? 'radio-active':''" :style="radiobakcolor"></view>
</view>
</view>
<view class="radio-content radio-right" @click="changeRadio2">
<text style="color: black;">否</text>
<view class="radio" :class="radio1 == 2 ? 'radio-default':''">
<view :class="radio1 == 2 ? 'radio-active':''" :style="radiobakcolor"></view>
</view>
</view>
</view>
</view>
</view>
<view class="cell-left">
<view class="cell-text">故障描述:</view>
</view>
<view class="cell-left">
<view class="cell-text">
<MyTextarea ref="content" pl="请在此处输入故障描述" :len='300' ></MyTextarea>
</view>
</view>
<view class="cell-left">
<view class="cell-text">照片上传:</view>
<view class="cell-text" style="color: #8C8C8C;width: 180px;">(最多上传3张图片)</view>
</view>
<view class="cell-left">
<view class="qtpicker">
<!-- 选中待上传的图片 -->
<view class="preImgs" v-for="(val,index) in preImgUrl" :key='index'>
<image style="border-radius: 6px;" mode="" :src="val" @click="showImg(val,index)">
</image>
<!-- 删除某张图片 -->
<!--
<view v-show="isShowDelImgBtn" class="cuo" @click="delImg(index)">×</view>
-->
<view v-show="isShowDelImgBtn">
<image class="cuo"
mode="" src="../../static/del_pic.png" @click="delImg(index)">
</image>
</view>
</view>
<!-- 添加图片的按钮 -->
<!--
<view class="addImg" v-show="isShowAddImgBtn" @click="chooseImg" style="border-radius: 9px;">
<view>+</view>
</view>
-->
<view v-show="isShowAddImgBtn">
<image style="width: 200rpx;height: 200rpx;margin-left: 13rpx;margin-top: 13rpx;"
@click="chooseImg"
mode="" src="../../static/add_pic.png">
</image>
</view>
</view>
</view>
</view>
<!-- 确定上传到服务器 -->
<button :disabled="submitForbide" type="primary" style="width: 50%;margin-top: 40px;" @click="submitImg">故障确认</button>
</view>
</template>
<script>
import MyTextarea from "@/components/my-textarea/my-textarea.vue"
import { toPicDetail ,
BASE_URL,
picSubmit,
deletePic} from '../../common/js/api.js'
import { getFaultConfirmInfo,submitFaultConfirm } from '@/common/js/api.js'
export default {
components: {
MyTextarea
},
data() {
return {
title: 'Hello',
radio1: 0,
preImgUrl: [], //本地预览的图片数据
imgNum: 0, //一共选中了多少张照片,用来限制本次最大上传数量
afterUploadPath: '', //上传图片后,返回上传后的图片地址入库用
isShowAddImgBtn: true,
content:'',
fault_id:'',
addoredit:'add',
editclick:0,//是否点击了右上角的编辑按钮
faultConfirmId:'',
isShowDelImgBtn:true,
submitForbide:false,
radiobakcolor:''
}
},
onLoad(opts) {
this.fault_id=opts.qry_faultid
this.getFaultConfirmInfoVue(opts.qry_faultid)
},
methods: {
getFaultConfirmInfoVue(qry_faultid) {
getFaultConfirmInfo({FAULT_ID: qry_faultid})
.then((res) => {
console.log("成功:"+res.listDataMap);
//如果能查询到核实记录,则回显
if(res.listDataMap.length>0){
//页面置为编辑状态
this.addoredit='edit'
let dataObj = res.listDataMap[0]
//禁用输入框和图片编辑和是否准确按钮,改变样式
this.radiobakcolor='background-color: grey;'
this.$refs["content"].textForbide = true
this.isShowDelImgBtn=false
this.submitForbide=true
//此条记录的id,更新用
this.faultConfirmId = dataObj.id
//故障描述
this.$refs["content"].content = dataObj.confirm_comment
//是否准确
this.radio1=dataObj.confirm_type
let imgPathStr = dataObj.img_path+""
//每一个文件的文件名:[xxx.jpg,yyy.png]
let imgPathArr = imgPathStr.split(",")
//拼接图片回显路径的头:http://localhost:8080/uploadPic/
let imgHeadPath = res.actionResult.message
//回显图片
if(imgPathStr.indexOf(",")!=-1){
//如果够3张了则隐藏添加图片按钮
this.imgNum=imgPathArr.length
if(this.imgNum==3){
this.isShowAddImgBtn=false
}
imgPathArr.forEach((item) => {
this.preImgUrl.push(imgHeadPath+item)
})
}
}
})
},
//这个方法很奇怪,在pages.vue里配上buttons,在页面上写上这个方法,点击就可以执行了
onNavigationBarButtonTap(e) {
console.log("点击编辑按钮")
//启用输入框和图片编辑和是否准确按钮,改变样式
this.radiobakcolor=''
this.editclick=1
this.$refs["content"].textForbide = false
this.isShowDelImgBtn=true
this.submitForbide=false
//如果要弹出筛选框
//this.$refs.popup.open()
},
// 选择图片
chooseImg() {
//如果是之前确认过,但是没点击编辑按钮,则直接return
if(this.addoredit=='edit'&&this.editclick==0){
return
}
let that = this
uni.chooseImage({
// count: 允许上传的照片数量
count: 3, //h5无法限制
// sizeType: original 原图,compressed 压缩图,默认二者都有
sizeType: "original,compressed",
success: function(res) { //选择成功,将图片存入本地临时路径,可删除,可查看,等待上传
console.log(res, '选择成功')
// 如果限制图片大小,则添加判断
res.tempFiles.map(val => {
// 判断本次上传限制的图片大小
if (val.size > 10485760) {
uni.showToast({
icon: 'none',
title: '上传的图片大小不超过10M'
})
return
}
// 判断本次最多上传多少照片
that.imgNum++
if(that.imgNum==3){
that.isShowAddImgBtn=false
uni.showToast({
icon: 'none',
title: '最多上传3张图片'
})
}
if (that.imgNum > 3) {
that.imgNum = 3
uni.showToast({
icon: 'none',
title: '上传的图片最多不能超过3张'
})
return
}
//把临时路径添加进数组,渲染到页面
that.preImgUrl.push(val.path)
})
}
})
},
// 确定上传图片,传到服务器
submitImg() {
let that = this
let fault_comment = that.$refs["content"].content;
if(that.radio1!=1&&that.radio1!=2){
uni.showToast({
icon: 'none',
title: '请选择故障是否准确'
})
return
}
if(fault_comment==""){
uni.showToast({
icon: 'none',
title: '请输入故障描述'
})
return
}
// 如果没有选择图片则返回,本次需求没有此规则,故注释掉
/*
if (that.preImgUrl.length == 0) {
uni.showToast({
icon: 'none',
title: '请选择图片'
})
return
}*/
// 获取图片地址的数量,用于实现同步请求
let len = that.preImgUrl.length
// 创建变量,用来创建定时器,用于实现同步请求
let timer = null
//阻塞阈值,因为使用定时器实现同步,所以需要防止无限循环,用于实现同步请求
let threshold = 3000;
// 遍历要上传的图片临时地址,进行上传
that.preImgUrl.map(val => {
uni.uploadFile({
url: BASE_URL + "dist/faultConfirm_uploadFaultPic.action", //服务器地址
filePath: val, //存在本地要上传的临时图片地址
name: 'file', //名字可以随便写
formData:{
"edit_Id" : this.faultConfirmId,
//故障ID,对应_FI表TYPE为0的线路故障ID
"FAULT_ID": this.fault_id,
//是否准确:0/1
"CONFIRM_TYPE": that.radio1,
//故障描述
"CONFIRM_COMMENT": fault_comment,
//故障核实状态:0/1
"CONFIRM_STATUS": "1",
//是编辑还是新增:add/edit
"ADDOREDIT": this.addoredit
},
success(res) { //上传成功的回调函数
//console.log(res, '上传图片成功')
//字符串转json
let myObj = JSON.parse(res.data)
//将上传后的图片路径拼接起来,入库用
that.afterUploadPath += myObj.actionResult.message+","
},
complete: (X)=>{
// 不论success还是fail,都把len-1
len--;
},
fail(res) {
console.log(res, '上传失败')
uni.showToast({
icon: 'none',
title: '上传失败'
})
}
})
})
// 关键部分
// 通过此部分来延续方法的时长,待到异步的回调执行完毕后则关闭
// 以防万一,建议设定一个阈值,防止出现意外导致无限循环
timer = setInterval(() => {
threshold--;
console.log(threshold);
// 只要len结束,将定时器清除
if(!len || threshold == 0 ) {
console.log("同步结束")
// 上传操作完成,消除阻塞
clearInterval(timer)
this.doSubmitFaultConfirm()
}
// 这个参数一定要写,如果不写在IOS端会报错导致同步失败
},1)
},
doSubmitFaultConfirm(){
//图片路径
let imgpath = this.afterUploadPath
//故障描述
let fault_comment = this.$refs["content"].content
//去掉最后一位逗号
imgpath = imgpath.substring(0,imgpath.length-1)
console.log("开始数据入库"+imgpath)
submitFaultConfirm({
"edit_Id" : this.faultConfirmId,
//故障ID,对应_FI表TYPE为0的线路故障ID
"FAULT_ID": this.fault_id,
//是否准确:0/1
"CONFIRM_TYPE": this.radio1,
//故障描述
"CONFIRM_COMMENT": fault_comment,
//故障核实状态:0/1
"CONFIRM_STATUS": "1",
//是编辑还是新增:add/edit
"ADDOREDIT": this.addoredit,
"IMG_PATH": imgpath
})
.then((res) => {
uni.showToast({
icon: 'none',
title: '故障核实提交成功'
})
})
},
//删除某张图片,从本地的临时路径图片中, 删除路径即可
delImg(index) {
this.imgNum--;
this.preImgUrl.splice(index, 1)
if(this.imgNum<3){
this.isShowAddImgBtn=true;
}
},
bindTextAreaBlur: function (e) {
console.log(e.detail.value)
},
changeRadio1() {
//如果是之前确认过,但是没点击编辑按钮,则直接return
if(this.addoredit=='edit'&&this.editclick==0){
return
}
this.radio1 = 1;
console.log("是")
},
changeRadio2() {
//如果是之前确认过,但是没点击编辑按钮,则直接return
if(this.addoredit=='edit'&&this.editclick==0){
return
}
this.radio1 = 2;
console.log("否")
},
//点击小图查看大图片
showImg(val, index) {
console.log(val, '点击了')
let that = this
uni.previewImage({
// 对选中的图片进行预览
urls: that.preImgUrl, //图片数组 // urls:['',''] 图片的地址 是数组形式
current: index, //当前图片的下标
})
}
}
}
</script>
<style scoped lang="scss">
* {
box-sizing: border-box;
}
.qtpicker {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 auto;
padding: 10rpx 0;
.preImgs {
margin: 13rpx;
position: relative;
image {
width: 200rpx;
height: 200rpx;
}
.cuo {
width: 17pt;
height: 17pt;
//line-height: 12px;
//text-align: center;
///* font-size: 16px; */
//border-radius: 50%;
//background-color: #223E4B;
//color: #FFFFFF;
position: absolute;
right: 0px;
top: 0px;
}
}
.addImg {
width: 200rpx;
height: 200rpx;
border: 2rpx solid #d4d4d4;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 20rpx;
background-color: #e2dddd;
margin-left: 13rpx;
margin-top: 13rpx;
}
}
.cell {
height: auto;
padding-left: 20rpx;
padding-right: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
background: #fff;
//border-bottom: 1px solid #BDD8F6;
.cell-left {
display: flex;
align-items: center;
padding: 8px;
.cell-icon {
width: 50rpx;
height: 50rpx;
}
.cell-text {
color: #595959 ;
font-size: 15px;
margin-left: 20rpx;
//width: 180rpx;
}
}
.iconfont {
font-size: 40rpx;
color: #999;
}
}
.radio-content {
height: 40rpx;
display: flex;
align-items: center;
}
.radio {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
border: 1rpx solid #CCCCCC;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0rpx 26rpx 0rpx 15rpx;
}
.radio-active{
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background-color: #0079FC;
}
.radio-default{
border: 2rpx solid #0079FC;
}
.radio-right {
margin-left: 36rpx;
}
</style>
多行文本输入框
<template>
<view class="r-top">
<textarea :disabled="textForbide" id="fault_comment" :class="textForbide ? 'textDisabled':''" style="border: 1px solid #ccc;" :maxlength="len" placeholder-style="color:rgba(153,153,153,1);" v-model="content" :placeholder="pl" />
<view class="r-top-t">
<view></view>
<view> {{contentLength}}/{{len}}字</view>
</view>
</view>
</template>
<script>
export default {
computed:{
contentLength(){
return this.content.length;
}
},
props:{
pl:{
type: String,
default: "请输入"
},
len:{
type: Number,
default: 200
}
},
data() {
return {
content:'',
textForbide:false
}
},
}
</script>
<style>
.r-top{
padding: 10rpx;
background: #FFFFFF;
border-radius: 20rpx;
}
.r-top-t{
display: flex;
justify-content: space-between;
font-family:PingFang SC;
font-weight:400;
line-height:20px;
color:rgba(153,153,153,1);
opacity:1;
}
.textDisabled{
border: 1px solid #ccc;
background-color: #D9D9D9 ;
}
</style>