技术标签: Vue+admin后台管理系统 elementui vue.js javascript
流程描述:使用el-upload组件,自定义上传方法(调后台接口),传图片file给后台,后台返回对应阿里云的oss链接,前端临时保存,最后点击页面提交按钮,再传后台oss数组链接。
项目遇到问题描述&解决:
- 将后台返回的oss直接赋值给el-upload对应的file-list属性绑定值对象的url字段时,会出现闪动问题。--默认组件绑定的url是blob链接,如果替换url取值,就会出现闪动问题。
- 设置multiple多选后,因后台接口限制1秒内不能有重复请求,添加时间戳参数,也会出现上传多张,部分图片的时间戳有的一样的问题,需使用uid(图片文件唯一标志)字段来代替时间戳。
其他待解决问题:(1)粘贴上传图片,不点击输入框,点击粘贴元素所在的行空白处,然后按ctrl+v也会触发粘贴paste事件,即也会自动上传,但此时点击确定所在行,再按ctrl+v又不触发了,失焦导致?
注意点:(1)需让后台在返回的文件URL中添加,唯一字符串后缀,如UUID等,确保图片文件名不会重复!
效果图:
<!--
* @Description: 上传图片组件(可粘贴上传)
* @Version:
* @Author: xxx
* @Date: 2022-10-11 10:26:13
* @LastEditors: Please set LastEditors
* @LastEditTime: 2022-10-10 13:33:20
-->
<template>
<div class="upload-imgs-box">
<div class="main-content">
<el-upload
ref="uploadRef"
list-type="picture-card"
action="#"
:accept="accept"
:class="{'upload__disabled': isDisableUpload}"
:auto-upload="true"
:multiple="multiple"
:file-list="allFileList"
:show-file-list="true"
:limit="limit"
:before-upload="handleBeforeUpload"
:http-request="uploadOSS"
:on-success="handleSuccess"
:on-error="handleError"
:on-remove="handleRemove"
:on-exceed="handleExceed"
:on-preview="handlePreview"
v-loading="loading"
element-loading-text="正在上传中,请稍候..."
element-loading-background="rgba(0, 0, 0, 0.5)"
>
<i class="el-icon-plus"></i>
</el-upload>
<!-- 显示复制元素 & 手机端 & 上传个数不大于限制时:才显示粘贴图片上传组件 -->
<div v-if="showCopy && !isMobile && (allFileList.length < limit)" class="copy_wrap mt-10" @paste="handlePaste" :contenteditable="false">
<input type="text" ref="copyInputRef" class="copy_main_content" autosize placeholder="点击粘贴图片到此处" maxlength="0">
</div>
<slot name="tip"></slot>
</div>
<el-dialog :visible.sync="showPreviewImage" append-to-body>
<img width="100%" :src="priviewImageUrl" alt="">
</el-dialog>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Ref } from 'vue-property-decorator'
import { isPC } from '@/utils/index'
import { getOSSUrlByFile } from '@/api/common'
// element中回调函数中返回的file
export interface ElFile {
name: string // 文件名:如:截屏2021-12-14 下午8.31.25.png
percentage: number
raw: File
size: number
status: 'fail' | 'ready' | 'success' // 文件的状态(3种)
uid: number // 文件唯一标志:ID
url: string // 如:"blob:http://localhost:9527/27499ca8-6ff1-4fa1-a3de-c64e4d8708e2"
oss?: string // 自己附加的属性
}
// 使用 http-request属性后,默认回调中返回的信息
interface HttpRequestInfo {
action?: string
file: File & { uid: number }
[key: string]: any
}
@Component({
name: 'UploadImgs'
})
export default class UploadImgs extends Vue {
@Prop({ default: 9 }) private limit!: number // 最多上传个数, 默认是9张
@Prop({ default: false }) private showCopy!: boolean // 是否需要展示粘贴图片插件,默认不展示
@Prop({ default: 5 }) private maxFileSize!: number // 图片上传的最大文件大小为(单位是兆,M),默认是5M
@Prop({ default: '.png,.PNG,.jpg,.JPG,jpeg,.JPEG,.bmp,.BMP,.gif,.GIF' }) private accept!: string // 可上传的图片类型
@Prop({ default: true }) private multiple!: boolean // 是否支持多选
@Prop({ default: () => [] }) private list!: string[] // 用于实时更新父组件的变量值
@Ref() copyInputRef!: HTMLElement // 复制图片的input
private priviewImageUrl = ''
private showPreviewImage = false
private disabled = false
private allFileList: Partial<ElFile>[] = []
private loading = false
get isMobile() {
return !isPC()
}
get isDisableUpload() {
return this.allFileList.length === this.limit
}
// 上传OSS:获取OSS链接
private uploadOSS(infoObj: HttpRequestInfo, isCopyUploadFlag: string) {
this.handleMultipleUpload(infoObj, isCopyUploadFlag)
}
private handleMultipleUpload(infoObj: HttpRequestInfo, isCopyUploadFlag: string) {
this.loading = true
const { file } = infoObj
const { uid, name } = file
const formData = new FormData()
formData.append('file', file)
formData.append('uid', uid + '')
formData.append('name', name)
formData.append('upload_time_stamp', Date.now() + '') // 注意:需让后台确保每个文件有一个唯一后缀名,防止文件名重名问题!
getOSSUrlByFile(formData).then(res => {
if (res.code === 200) {
if (isCopyUploadFlag === 'isCopy') {
this.copyInputRef.blur() // 移除复制图片框中的光标
const { oss } = res.data
if (oss) {
this.allFileList.push({ url: oss })
this.$emit('update:list', this.allFileList.map(item => item?.url))
}
} else {
infoObj.onSuccess(res)
}
} else {
const isObj = Object.prototype.toString.call(res) === '[object Object]' // true 代表为对象
let tip = ''
if (isObj) {
if (Object.keys(res).length === 0) {
tip = '1个账号不能多端登录或其他原因'
} else {
tip = res.message || JSON.stringify(res)
}
} else {
tip = res
}
this.$message.warning(`上传失败, 原因可能为:${tip}`)
infoObj.onError(res)
}
}).finally(() => {
this.loading = false
})
}
private handleSuccess(res: { code: number, data: { oss: string } }, file: ElFile, fileList: ElFile[]) {
const { oss } = res.data
console.log('文件上传成功', res, file, fileList)
if (res.code === 200) {
file.oss = oss // 注意:此处不能直接将后台返回的oss赋值给file.url字段,会出现闪动现象。
this.allFileList = fileList
this.$emit('update:list', this.allFileList.map(item => item?.oss)) // 更新父组件的变量值
} else {
console.log('接口报错非200')
}
}
private handleError() {
// 注意:该回调函数必须写,不然:上传失败,页面上还显示图片。写了之后:页面会自动删除失败的图片。
console.log('文件上传失败')
}
private handleRemove(file: ElFile, fileList: ElFile[]) {
this.allFileList = fileList
this.$emit('update:list', this.allFileList.map(item => item?.oss || item?.url)) // 更新父组件的变量值
console.log('文件删除了', this.allFileList, file, fileList)
}
// 上传前:钩子函数
private handleBeforeUpload(file: File) {
const { size } = file
const isLessLimitMaxFileSize = size / 1024 / 1024 < this.maxFileSize // 将文件大小转为M单位,并跟最大限制做比较
if (!isLessLimitMaxFileSize) {
this.$message.warning(`上传图片大小,不能超过 ${this.maxFileSize}M!`)
return false
} else {
return true
}
}
// 文件勾选超出限制
private handleExceed(files: File[], fileList: ElFile[]) {
const emptyNum = this.limit - (fileList.length)
console.log('目前还能上传个数为', emptyNum)
this.$message.warning(`上传文件个数,超出限制(最多${this.limit}张)`)
}
// 图片粘贴触发事件
private handlePaste(event: any) {
const items = (event.clipboardData || (window as any).clipboardData).items
// 去除粘贴到div事件
event.preventDefault()
event.returnValue = false
let file = null
if (!items || items.length === 0) {
this.$message.warning('当前不支持本地图片上传')
return
}
// 搜索剪切板items
for (let i = 0; i < items.length; i++) {
if (items[i].type.includes('image')) {
file = items[i].getAsFile()
break
}
}
if (!file) {
this.$message.warning('粘贴内容非图片')
return
}
if (!this.handleBeforeUpload(file)) {
return false
}
this.uploadOSS({ file: file }, 'isCopy')
}
// 预览:当前图片
private handlePreview(file: ElFile) {
this.priviewImageUrl = file.url
this.showPreviewImage = true
}
}
</script>
<style lang="scss" scoped>
.upload-imgs-box {
.copy_wrap {
width: 148px;
height: 148px;
text-align: center;
.copy_main_content {
width: 148px;
height: 148px;
text-align: center;
border: 1px dashed #c0ccda;
border-radius: 6px;
outline-color: #409EFF; // 设置input点击后出现的边框的颜色
}
}
.upload__disabled {
::v-deep .el-upload--picture-card {
display: none; // 隐藏上传组件
}
}
}
</style>
子组件中通过 $emit('update:prop变量名', '传递给父组件的新值') 来实时更新父组件的数据。注意:冒号后不能有空格!否则不生效。
<upload-imgs ref="upload_image_box" :list.sync="parentForm.imgsList" :showCopy="false"></upload-imgs>
通过 ref 获取上传组件实例,从而获取对应上传url列表数组数据。
<template>
<div>
<upload-imgs ref="upload_image_box" :showCopy="false">
<div class="tip-color">最多上传9张图片,只支持jpg、png、jpeg、bmp或gif格式,且单个图片文件不能超过5M。</div>
</upload-imgs>
<el-button type="primary" @click="submit">确定</el-button>
</div>
</template>
<script lang="ts">
... // 省略code
interface UploadImageComponent {
allFileList: Partial<ElFile>[] // 最后上传的所有图片数组[{ url: 'http:xxx', ... }, { oss: 'http:xx', ... }]
}
@Ref('upload_image_box') readonly uploadImageCompoent!: UploadImageComponent
private submit() {
const { allFileList = [] } = this.uploadImageCompoent
... // 省略code
const imagesList: string[] = []
allFileList.forEach((item: Partial<ElFile>) => {
if (item.oss) {
imagesList.push(item.oss) // 通过el-uplod上传的
} else if (item.url) {
imagesList.push(item.url) // 通过复制粘贴上传的
}
})
// 最后提交给后台的参数:files为所有oss图片链接的数组字段。
const params = { files: imagesList, .... }
... // 调后台接口
}
</script>
// 判断是否是PC端显示
export const isPC = () => {
const userAgentInfo = navigator.userAgent
const Agents = ['Android', 'iPhone', 'SymbianOS', 'Windows Phone', 'iPad', 'iPod']
let isPCFlag = true
for (let v = 0; v < Agents.length; v++) {
if (userAgentInfo.indexOf(Agents[v]) > 0) {
isPCFlag = false
break
}
}
return isPCFlag
}
// 上传File文件获取OSS链接(仅单个,不支持批量!)
export const getOSSUrlByFile = (data = {}) => {
return request({ // axios库相关封装的实现,注意需判断formdata类型的post请求不能序列化。
url: 'xxxxxxxx',
headers: {
'Content-Type': 'multipart/form-data'
},
method: 'post',
data
})
}
// 上传类接口(传参为FormData类型的):不能序列化,否则会被序列化为空对象!
... // axios请求拦截器
if (request.method === 'post' && request.data) {
if (!(request.data instanceof FormData)) {
request.data = qs.stringify(request.data)
}
}
文章浏览阅读193次。转自:http://hi.baidu.com/bitbull/blog/item/bc27581eca1886f61bd5768e.html问题概要: 用lua写了个函数,返回的是一个表.需要在C里对返回的表里元素做二次处理.在C里我们可以通过lua_gettable()或者lua_rawget()来获取表里元素值,但使用这两个接口的前提是你得知道key,它才能给你value. 当然对于顺..._lua_next遍歷
文章浏览阅读832次。import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import org.apache.commons.lang3.StringUtils;/** * 关键字替换类 * */public class Keywor..._java 多个关键词全词匹配替换
文章浏览阅读3.7w次,点赞20次,收藏59次。View 提供了如下 5 种方法获取 View 的坐标:1. View.getTop()、View.getLeft()、View.getBottom()、View.getRight();2. View.getX()、View.getY();3. View.getTranslationX()、View.getTranslationY();4. View.getLocationOnScreen(_findviewbyposition
文章浏览阅读817次。详细说明QQmlProperty类抽象了从QML创建的对象的访问属性。由于QML使用Qt的元类型系统,因此所有现有的QMetaObject类都可以用于自省和与QML创建的对象进行交互。但是,QML提供的一些新功能(例如类型安全性和附加属性)最容易通过QQmlProperty类使用,从而简化了某些自然复杂性。与QMetaProperty代表类类型的属性不同,QQmlProperty封装特定对象实例上的属性。为了读取属性的值,程序员创建一个QQmlProperty实例并调用read()方法。同样地,使用w_qqmlproperty::read
文章浏览阅读321次。转自: http://www.cnblogs.com/HondaHsu/archive/2007/06/20/776303.html常量 1. 定义常量的语法格式 常量名 constant 类型标识符 [not null]:=值; 常量,包括后面的变量名都必须以字母开头,不能有空格,不能超过30个字符长度,同时不能和保留字同名,常(变)量名称不区分大小_\pl/sql的基本语法:_____________、_____________、_____________、_____________
文章浏览阅读1.5k次。QAudioRecorder类用于记录音频。QAudioRecorder类是高级媒体录制类,并且包含与QMediaRecorder相同的功能。 audioRecorder = new QAudioRecorder; QAudioEncoderSettings audioSettings; audioSettings.setCodec("audio/amr"); audioSettings.setQuality(QMultimedia::HighQuality); audioRecor_qaudiorecorder
文章浏览阅读193次。2、enableNative的分析 enable Native是真正的蓝牙使能的函数,蓝牙打开的一系列操作都是通过他来真正实现的。可以认为,这个函数蓝牙使能的主干,其余几个方面都可以认为是旁枝末节而已,因此,无论如何,我们必须了解到这个函数真正的精髓所在。 先来看jni层究竟是如何实现这个函数的:static jint enableNative(JNIEnv *env, jobje..._hci_dev没有voice_setting 成员
文章浏览阅读200次。typora-root-url: imagesAdminGuideCanal-Admin-Guide简介canal [kə’næl],译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。工作原理canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送 dump 协议MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal._canal使用心得
文章浏览阅读295次。由于近期可能需要观察程序运行的内存以及线程等指标,需要监控在Docker中java应用,在尝试与之前的方式添加后,并不能生效,因此在这里记录,和大家分享。需要添加JMX配置如下-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local_-dcom.sun.management.jmxremote=true
文章浏览阅读466次。 因项目需要将几个word文件合并到一个word文件,后面附项目运用的jar包jacob-1.9jacob运用中,需要将附件内的jacob.dll放到windows/system32下 直接上代码:public static void main(String[] args) { List list = new ArrayList(); String f..._jacob insertfile hebing
文章浏览阅读75次。1.用MJRefresh框架实现上下拉刷新1.1如何使用这个框架,只需要告诉控件的scrollView是谁,就能将框架添加到我们的滚动视图中了//下拉刷新MJRefreshHeaderView*header = [MJRefreshHeaderViewheader];header.scrollView=self.tableView;...
文章浏览阅读115次。在SQL Server 2005/2008中,维护计划的功能通过SSIS包来完成。如果不小心在SSIS管理中删除了"Maintenance Plans"文件夹,则在SQL Server中建立维护计划的时候会出现问题,如下图所示。 在Management studio中创建误删除的"Maintenance Plans"文件夹,如下图所示。 建立好"Maintenance Plan..._package=maintenance plans 路径