bootstrap4.x

bootstrap4.x

bootstrap4.x+vue2.x 文件上传

VUElopo1983 发表了文章 • 0 个评论 • 1203 次浏览 • 2018-03-16 22:43 • 来自相关话题

template<template>
<div class="upload">
<label class="mb-0" :class="hasSlot ? '' : 'custom-file'" @drop.prevent="onDrop">
<input type="file" :class="hasSlot ? 'd-none' : 'custom-file-input'" @change="onPicker" :multiple="multiple" :accept="accept" v-if="refresh">
<span v-if="hasSlot"><slot></slot></span>
<span v-else class="custom-file-control">选择文件</span>
</label>
<span class="help text-secondary" :class="!!helpblock?`d-block mt-2`:'ml-3'" v-if="help">{{help}}</span>
<cardlayer :class="{'mt-3':files.length}">
<card class="upload-item" v-for="(item,idx) in files" :key="idx" :style="`flex: 0 0 ${1/col*100}%`">
<cardbody>
<div class="upload-cancel">
<div class="icon-mix">
<icon icon="shanchu" @click.native="onCancel(idx)" title="删除"></icon>
<icon icon="reupload" @click.native="upload" v-if="item.stype=='danger'" title="重传"></icon>
</div>
</div>
<div class="upload-preview" :style="{backgroundImage:`url(${item._base64 || item.base64})`}"></div>
<img class="card-img-top" :src="square">
</cardbody>
<cardfooter :class="item.stype!=''&&`bg-${item.stype}`">
<progressbar class="upload-progress" v-bind="item" size="xs"></progressbar>
<div class="d-flex upload-info ">
<div class="text-truncate mb-0 float-left">{{item.name}}</div>
</div>
</cardfooter>
</card>
</cardlayer>
<!--<div class="file-holder">
<div v-for="(item,idx) in files" :key="idx">
<a href="javascript:;" @click="onCancel(idx)">取消</a>
<div class="holder" :style="{backgroundImage:`url(${item.base64})`}">{{ item.name }}</div>
<progressbar v-bind="item" size="xs"></progressbar>
</div>
</div>-->
<button type="button" class="btn btn-ces mt-3" @click="upload" v-if="!autoUpload&&files.length">上传</button>
</div>
</template>javascript<script>
/**
* bootstrap 4.x --> components --> uploader
*
* @param {Boolean} multiple 多选
* @param {Number} max 多选最大值
* @param {Array} maxSize 最大尺寸[width,height] ,不限制不传或者传 0
* @param {Array} allowMime 允许上传的 MIME 类型,默认不限制
* @param {String} url 上传URL
* @param {Boolean} autoUpload 是否选择文件后自动上传
* @param {Function} uploadMethod 自定义上传方法,必须返回Promise,成功resolve
* @param {Function|Object} extraParams
* @param {String} help 上传帮助文字
*
* @event remove 删除
* @event error 错误
* @event success 单个上传成功
* @event completed 全部上传成功
*
* @date 2017-10-24
*
* @requires 依赖 axios icon cards
*
* @version v1.0.0 beta
*
* @author www.bsfans.com 戏子 lopo
*
**/
import icon from './icon'
import progressbar from './progress'
import square from '@/assets/square.png'
import {
cardlayer,
card,
cardheader,
cardbody,
cardfooter
} from '@/components/comp/cards'
import axios from 'axios'

const IMAGE_MIME = ['image/png', 'image/jpg', 'image/gif', 'image/jpeg']

const fnTypeBase64 = text => {
const canvas = document.createElement('canvas')
canvas.width = 200
canvas.height = 200
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'rgba(255,255,255,0.8)'
ctx.fillRect(0, 0, 200, 200)
ctx.stroke()
ctx.font = '40px monaco'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(text, 100, 100)
return canvas.toDataURL('image/jpeg')
}

export default {
name: 'uploader',
props: {
multiple: Boolean,
max: Number,
maxSize: {
type: Array,
default: () =>
},
allowMime: {
type: Array,
default: () =>
},
col: {
type: Number,
default: 6
},
help: String,
helpblock:{
type:Boolean,
default:false
},
url: String,
autoUpload: {
type: Boolean,
default: false
},
uploadMethod: Function,
extraParams: [Function, Object]
},
data() {
return {
files: ,
refresh: true,
square: square
}
},
components: {
progressbar,
icon,
cardlayer,
card,
cardheader,
cardbody,
cardfooter
},
methods: {
onPicked(files) {
if (this.max > 0 && this.files.length + files.length > this.max) {
this.$emit('error','图片太大')
return false
}
const isImage = type => IMAGE_MIME.findIndex(item => item == type) > -1
this.$emit('picked', files)
Array.from(files).forEach(file => {
const o = {
file,
name: file.name,
filesize: file.size,
type: file.name
.substring(file.name.lastIndexOf('.'))
.toLowerCase()
.replace('.', ''),
stype: '',
value: 0,
base64: '',
width: 0,
height: 0,
isimage: isImage(),
completed: false,
uploading: false
}

if (this.allowMime.length) {
if (this.allowMime.findIndex(item => item == file.type) === -1) {
return this.$emit('error')
}
}

const reader = new FileReader()
reader.onload = e => {
o.base64 = e.target.result
const idx = this.files.findIndex(item => {
return item.base64 == o.base64 && item.name == o.name
})
if (idx === -1) {
if (isImage(file.type)) {
const image = new Image()
image.onload = () => {
o.width = image.width
o.height = image.height
const [maxwidth, maxheight] = this.maxSize
if (maxwidth && maxwidth < o.width) {
this.$emit('error')
} else if (maxheight && maxheight < o.height) {
this.$emit('error')
} else {
this.files.push(o)
this.autoUpload && this.fnUpload(o)
}
}
image.src = o.base64
} else {
o._base64 = fnTypeBase64(o.type)
this.autoUpload && this.fnUpload(o)
this.files.push(o)
}
}
}
reader.onerror = () => {
this.$emit('error')
}
reader.readAsDataURL(file)
})
this.reset()
},
reset() {
this.refresh = false
this.$nextTick(() => {
this.refresh = true
})
},
onPicker(e) {
this.onPicked(e.target.files)
},
onDrop(e) {
this.onPicked(e.dataTransfer.files)
},
onCancel(idx) {
this.files.splice(idx, 1)
this.$emit('remove', idx)
},
upload() {
this.files.filter(item => !item.uploading).forEach(this.fnUpload)
},
clearFile() {
this.files =
},
fnUpload(o) {
o.uploading = true
if (this.uploadMethod) {
const p = this.uploadMethod(o)
if (p.then) {
p.then(res => {
o.stype = 'success'
o.value = 100
o.completed = true
this.$emit('success', res)
})
}
} else {
// console.log('no then')
const formData = new FormData()
formData.append('file', o.file)
const params =
typeof this.extraParams == 'function'
? this.extraParams(o)
: this.extraParams
Object.keys(params).forEach(key => formData.append(key, params[key]))
axios
.post(this.url, formData, {
onUploadProgress(evt) {
o.value = 100 * evt.loaded / evt.total
}
})
.then(
res => {
o.stype = 'success'
o.completed = true
this.$emit('success', res)
},
err => {
o.stype = 'danger'
this.$emit('error')
}
)
}
}
},
computed: {
accept() {
return this.allowMime.length ? this.allowMime.join(',') : ''
},
isCompleted() {
if (this.files.length) {
return this.files.filter(item => !item.completed).length == 0
} else {
return false
}
},
hasSlot() {
return !!this.$slots.default
}
},
watch: {
isCompleted(v) {
v && this.$emit('completed')
}
},
mounted() {
const DRAG_EVENTS = ['dragleave', 'drop', 'dragenter', 'dragover']
DRAG_EVENTS.forEach(evtName => {
document.addEventListener(evtName, e => e.preventDefault(), false)
})
}
}
</script>style(less)<style lang="less">
@import (reference) '../../assets/lib/css.less';
.upload {
input[type='file'] {
opacity: 0;
}
.custom-file {
.mgb(1rem);
}
&-progress {
.ps;
top: 0;
left: 0;
right: 0;
}
&-info {
.cp;
align-items: center;
}
/*&-progress {
.ppd;
width:0;
z-index: 2;
.bgc(@ces);
}*/
&-item {
.card-body {
overflow: hidden;
position: relative;
z-index: 1;
}
.card-footer {
.trs;
.pr;
.bgcw;
.pdy(0.3rem);
&[class*='bg'] {
.crw;
}
}
}
&-preview {
.ppd(1.25rem);
.bdr(@cr: rgba(0, 0, 0, 0.1));
background-repeat: no-repeat;
background-position: center center;
background-size: 100%;
}
&-cancel {
.mask;
.trs;
opacity: 0;
.bgc(rgba(0, 0, 0, 0.5));
z-index: 3;
.icon-mix {
.amid;
.iconfont + .iconfont {
.mgl(1.5rem);
}
}
.iconfont {
.fs(1.6rem);
.cp;
.trs;
.trf(scale(0));
.crw;
.trfo(50% 50%);
&:after {
content: '';
.bgc(rgba(0, 0, 0, 0.2));
width: 3rem;
height: 3rem;
.db;
.mask;
.trs;
z-index: -1;
left: -0.7rem;
top: -0.3rem;
.bdrrd(3rem);
}
&:hover:after {
.bgc(@ces);
}
}
.upload .card:hover & {
opacity: 1;
.iconfont {
.trf(scale(1));
}
}
}
}
</style>




? 查看全部

QQ图片20180316224810.png

template
<template>
<div class="upload">
<label class="mb-0" :class="hasSlot ? '' : 'custom-file'" @drop.prevent="onDrop">
<input type="file" :class="hasSlot ? 'd-none' : 'custom-file-input'" @change="onPicker" :multiple="multiple" :accept="accept" v-if="refresh">
<span v-if="hasSlot"><slot></slot></span>
<span v-else class="custom-file-control">选择文件</span>
</label>
<span class="help text-secondary" :class="!!helpblock?`d-block mt-2`:'ml-3'" v-if="help">{{help}}</span>
<cardlayer :class="{'mt-3':files.length}">
<card class="upload-item" v-for="(item,idx) in files" :key="idx" :style="`flex: 0 0 ${1/col*100}%`">
<cardbody>
<div class="upload-cancel">
<div class="icon-mix">
<icon icon="shanchu" @click.native="onCancel(idx)" title="删除"></icon>
<icon icon="reupload" @click.native="upload" v-if="item.stype=='danger'" title="重传"></icon>
</div>
</div>
<div class="upload-preview" :style="{backgroundImage:`url(${item._base64 || item.base64})`}"></div>
<img class="card-img-top" :src="square">
</cardbody>
<cardfooter :class="item.stype!=''&&`bg-${item.stype}`">
<progressbar class="upload-progress" v-bind="item" size="xs"></progressbar>
<div class="d-flex upload-info ">
<div class="text-truncate mb-0 float-left">{{item.name}}</div>
</div>
</cardfooter>
</card>
</cardlayer>
<!--<div class="file-holder">
<div v-for="(item,idx) in files" :key="idx">
<a href="javascript:;" @click="onCancel(idx)">取消</a>
<div class="holder" :style="{backgroundImage:`url(${item.base64})`}">{{ item.name }}</div>
<progressbar v-bind="item" size="xs"></progressbar>
</div>
</div>-->
<button type="button" class="btn btn-ces mt-3" @click="upload" v-if="!autoUpload&&files.length">上传</button>
</div>
</template>
javascript
<script>
/**
* bootstrap 4.x --> components --> uploader
*
* @param {Boolean} multiple 多选
* @param {Number} max 多选最大值
* @param {Array} maxSize 最大尺寸[width,height] ,不限制不传或者传 0
* @param {Array} allowMime 允许上传的 MIME 类型,默认不限制
* @param {String} url 上传URL
* @param {Boolean} autoUpload 是否选择文件后自动上传
* @param {Function} uploadMethod 自定义上传方法,必须返回Promise,成功resolve
* @param {Function|Object} extraParams
* @param {String} help 上传帮助文字
*
* @event remove 删除
* @event error 错误
* @event success 单个上传成功
* @event completed 全部上传成功
*
* @date 2017-10-24
*
* @requires 依赖 axios icon cards
*
* @version v1.0.0 beta
*
* @author www.bsfans.com 戏子 lopo
*
**/
import icon from './icon'
import progressbar from './progress'
import square from '@/assets/square.png'
import {
cardlayer,
card,
cardheader,
cardbody,
cardfooter
} from '@/components/comp/cards'
import axios from 'axios'

const IMAGE_MIME = ['image/png', 'image/jpg', 'image/gif', 'image/jpeg']

const fnTypeBase64 = text => {
const canvas = document.createElement('canvas')
canvas.width = 200
canvas.height = 200
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'rgba(255,255,255,0.8)'
ctx.fillRect(0, 0, 200, 200)
ctx.stroke()
ctx.font = '40px monaco'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(text, 100, 100)
return canvas.toDataURL('image/jpeg')
}

export default {
name: 'uploader',
props: {
multiple: Boolean,
max: Number,
maxSize: {
type: Array,
default: () =>
},
allowMime: {
type: Array,
default: () =>
},
col: {
type: Number,
default: 6
},
help: String,
helpblock:{
type:Boolean,
default:false
},
url: String,
autoUpload: {
type: Boolean,
default: false
},
uploadMethod: Function,
extraParams: [Function, Object]
},
data() {
return {
files: ,
refresh: true,
square: square
}
},
components: {
progressbar,
icon,
cardlayer,
card,
cardheader,
cardbody,
cardfooter
},
methods: {
onPicked(files) {
if (this.max > 0 && this.files.length + files.length > this.max) {
this.$emit('error','图片太大')
return false
}
const isImage = type => IMAGE_MIME.findIndex(item => item == type) > -1
this.$emit('picked', files)
Array.from(files).forEach(file => {
const o = {
file,
name: file.name,
filesize: file.size,
type: file.name
.substring(file.name.lastIndexOf('.'))
.toLowerCase()
.replace('.', ''),
stype: '',
value: 0,
base64: '',
width: 0,
height: 0,
isimage: isImage(),
completed: false,
uploading: false
}

if (this.allowMime.length) {
if (this.allowMime.findIndex(item => item == file.type) === -1) {
return this.$emit('error')
}
}

const reader = new FileReader()
reader.onload = e => {
o.base64 = e.target.result
const idx = this.files.findIndex(item => {
return item.base64 == o.base64 && item.name == o.name
})
if (idx === -1) {
if (isImage(file.type)) {
const image = new Image()
image.onload = () => {
o.width = image.width
o.height = image.height
const [maxwidth, maxheight] = this.maxSize
if (maxwidth && maxwidth < o.width) {
this.$emit('error')
} else if (maxheight && maxheight < o.height) {
this.$emit('error')
} else {
this.files.push(o)
this.autoUpload && this.fnUpload(o)
}
}
image.src = o.base64
} else {
o._base64 = fnTypeBase64(o.type)
this.autoUpload && this.fnUpload(o)
this.files.push(o)
}
}
}
reader.onerror = () => {
this.$emit('error')
}
reader.readAsDataURL(file)
})
this.reset()
},
reset() {
this.refresh = false
this.$nextTick(() => {
this.refresh = true
})
},
onPicker(e) {
this.onPicked(e.target.files)
},
onDrop(e) {
this.onPicked(e.dataTransfer.files)
},
onCancel(idx) {
this.files.splice(idx, 1)
this.$emit('remove', idx)
},
upload() {
this.files.filter(item => !item.uploading).forEach(this.fnUpload)
},
clearFile() {
this.files =
},
fnUpload(o) {
o.uploading = true
if (this.uploadMethod) {
const p = this.uploadMethod(o)
if (p.then) {
p.then(res => {
o.stype = 'success'
o.value = 100
o.completed = true
this.$emit('success', res)
})
}
} else {
// console.log('no then')
const formData = new FormData()
formData.append('file', o.file)
const params =
typeof this.extraParams == 'function'
? this.extraParams(o)
: this.extraParams
Object.keys(params).forEach(key => formData.append(key, params[key]))
axios
.post(this.url, formData, {
onUploadProgress(evt) {
o.value = 100 * evt.loaded / evt.total
}
})
.then(
res => {
o.stype = 'success'
o.completed = true
this.$emit('success', res)
},
err => {
o.stype = 'danger'
this.$emit('error')
}
)
}
}
},
computed: {
accept() {
return this.allowMime.length ? this.allowMime.join(',') : ''
},
isCompleted() {
if (this.files.length) {
return this.files.filter(item => !item.completed).length == 0
} else {
return false
}
},
hasSlot() {
return !!this.$slots.default
}
},
watch: {
isCompleted(v) {
v && this.$emit('completed')
}
},
mounted() {
const DRAG_EVENTS = ['dragleave', 'drop', 'dragenter', 'dragover']
DRAG_EVENTS.forEach(evtName => {
document.addEventListener(evtName, e => e.preventDefault(), false)
})
}
}
</script>
style(less)
<style lang="less">
@import (reference) '../../assets/lib/css.less';
.upload {
input[type='file'] {
opacity: 0;
}
.custom-file {
.mgb(1rem);
}
&-progress {
.ps;
top: 0;
left: 0;
right: 0;
}
&-info {
.cp;
align-items: center;
}
/*&-progress {
.ppd;
width:0;
z-index: 2;
.bgc(@ces);
}*/
&-item {
.card-body {
overflow: hidden;
position: relative;
z-index: 1;
}
.card-footer {
.trs;
.pr;
.bgcw;
.pdy(0.3rem);
&[class*='bg'] {
.crw;
}
}
}
&-preview {
.ppd(1.25rem);
.bdr(@cr: rgba(0, 0, 0, 0.1));
background-repeat: no-repeat;
background-position: center center;
background-size: 100%;
}
&-cancel {
.mask;
.trs;
opacity: 0;
.bgc(rgba(0, 0, 0, 0.5));
z-index: 3;
.icon-mix {
.amid;
.iconfont + .iconfont {
.mgl(1.5rem);
}
}
.iconfont {
.fs(1.6rem);
.cp;
.trs;
.trf(scale(0));
.crw;
.trfo(50% 50%);
&:after {
content: '';
.bgc(rgba(0, 0, 0, 0.2));
width: 3rem;
height: 3rem;
.db;
.mask;
.trs;
z-index: -1;
left: -0.7rem;
top: -0.3rem;
.bdrrd(3rem);
}
&:hover:after {
.bgc(@ces);
}
}
.upload .card:hover & {
opacity: 1;
.iconfont {
.trf(scale(1));
}
}
}
}
</style>

QQ图片20180316224810.png

?

vue+bootstrap4+tooltip.js 实现简单的tooltip

VUElopo1983 发表了文章 • 0 个评论 • 1976 次浏览 • 2018-01-24 17:33 • 来自相关话题

<template lang="pug">
button(:class="`btn btn-${size} btn-${stype}`",ref="button",@mouseover="initPopper")
<slot></slot>
<template v-if="!$slots.default">{{label}}</template>
</template>
<script>
import lib from '@/utils/lib'
import btn from '@/components/comp/button'
import Tooltip from 'tooltip.js'
export default {
name: 'vbaToolTip',
components: {
btn
},
props: {
label: String,
size: String,
stype: String,
placement: {
type: String,
default: 'top'
},
html: {
type: Boolean,
default: false
},
content: String
},
data() {
return {
popperInstance: null
}
},
methods: {
initPopper() {
if (!this.popperInstance) {
const vm = this
this.popperInstance = new Tooltip(this.$refs.button, {
placement: `${this.placement}`,
template: `<div class="tooltip bs-tooltip-${
this.placement
}" role="tooltip">
<div class="tooltip-arrow arrow"></div>
<div class="tooltip-inner">
</div>
</div>`,
title: this.content,
html:this.html,
contaier:document.getElementsByTagName('body'),
onCreate() {
vm.$emit('on-create', this.popperInstance)
},
onUpdate() {
vm.$emit('on-update', this.popperInstance)
}
})
}
}
}
}
</script>
<style lang="less">
.tooltip {
opacity: 1 !important;
}
</style> 查看全部
<template lang="pug">
button(:class="`btn btn-${size} btn-${stype}`",ref="button",@mouseover="initPopper")
<slot></slot>
<template v-if="!$slots.default">{{label}}</template>
</template>
<script>
import lib from '@/utils/lib'
import btn from '@/components/comp/button'
import Tooltip from 'tooltip.js'
export default {
name: 'vbaToolTip',
components: {
btn
},
props: {
label: String,
size: String,
stype: String,
placement: {
type: String,
default: 'top'
},
html: {
type: Boolean,
default: false
},
content: String
},
data() {
return {
popperInstance: null
}
},
methods: {
initPopper() {
if (!this.popperInstance) {
const vm = this
this.popperInstance = new Tooltip(this.$refs.button, {
placement: `${this.placement}`,
template: `<div class="tooltip bs-tooltip-${
this.placement
}" role="tooltip">
<div class="tooltip-arrow arrow"></div>
<div class="tooltip-inner">
</div>
</div>`,
title: this.content,
html:this.html,
contaier:document.getElementsByTagName('body'),
onCreate() {
vm.$emit('on-create', this.popperInstance)
},
onUpdate() {
vm.$emit('on-update', this.popperInstance)
}
})
}
}
}
}
</script>
<style lang="less">
.tooltip {
opacity: 1 !important;
}
</style>

vue-form + bootstrap4 beta 二次封装

VUElopo1983 发表了文章 • 1 个评论 • 1285 次浏览 • 2017-12-22 10:54 • 来自相关话题

Form 外层组件<template>
<vue-form v-if="!unform" :class="{'form-inline':inline}" :state="state" v-model="state" @submit.prevent="formMethod" >
<slot></slot>
</vue-form>
<div class="form" v-else>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'vbaForm',
provide() {
return {
vbaForm: this
}
},
props: {
inline: Boolean,
unform: {
type: Boolean,
default: false
},
state: {},
formMethod: Function
},
methods: {
fieldClassName(field) {
if (!field) {
return ''
}
if ((field.$touched || field.$submitted) && field.$valid) {
return 'has-success'
}
if ((field.$touched || field.$submitted) && field.$invalid) {
return 'has-error'
}
}
}
}
</script>
<style lang="less">
@import (reference) '../../../assets/lib/css.less';
form,
div {
.form-group:last-child {
.mgb(0);
}
}
</style>


FormItem<template lang="pug">
validate.form-group.required-field.row.mb-4(v-if="!!isForm",auto-label,:class='vbaForm.fieldClassName(`${$parent.formstate}[${prop}]`)')
label.col.col-form-label(:class="{'text-right':right}",v-if="!!col||col===12") {{label}}
cols(:col="col",:size="size")
slot
field-messages.help-block.text-danger.position-absolute.mb-0(:name='prop', show='$touched || $submitted',tag="section")
div(v-for="(item,index) in messages",:slot="item.slot") {{item.label}}
rows.form-group(v-else)
label.col.col-form-label(:class="{'text-right':right}",v-if="!!col||col===12") {{label}}
cols.d-flex.align-items-center(:col="col",:size="size")
slot
</template>
<script>
import { rows, cols } from '@/components/layout'
export default {
name: 'formItem',
components: {
rows,
cols
},
inject: ['vbaForm'],
model:{
prop:'messages'
},
props: {
messages:[Array],
label: String,
right: {
type: Boolean,
default: false
},
col: [Number, Array, String],
size: [String, Array],
prop: String,
unForm:{
type:Boolean,
default:false
}
},
data() {
return {}
},
computed: {
isForm() {
return !this.unForm?!this.vbaForm.unform && this.prop:true
}
}
}
</script>


使用案例方法<template lang="pug">
section#authentication.uc-panel
.d-flex.mt-4
step.w-50.mx-auto(:names="stepData",:current="step")
.d-flex.w-500.mx-auto.my-3
panel.w-100(:stype="step===3?2:3")
Forms(:state="formstate",:formMethod="sendPassword",ref="sendinfo",v-if="step!=3")
template(v-if="step===1")
FormItem(:label="getType==='phone'?'您的手机:':'您的邮箱'",right,:col="9",:unForm="true")
.input-group
input.form-control-plaintext(type="text",readonly,:name="getType",:value="getType==='phone'?$store.state.user.user.phone:$store.state.user.user.email")
span.input-group-btn
timer.btn.btn-sm.btn-secondary(:begin="timer",:second="time",@clevent='getSmsCode')
FormItem(label="您的验证码:",:col="9",right,prop="code",key="code",v-model="rules.code")
input.form-control(type='text',name='code',autocomplete="off",required,v-model.lazy='sendModal.code')
FormItem(:col="9")
button.btn.btn-sm.btn-info.px-3(type="submit") 下一步
template(v-else-if="step===2")
FormItem(label="旧密码:",:col="9",right,prop="oldpassword",key="oldpassword",v-model="rules.oldpassword")
input.form-control(type='password',name='oldpassword',required,v-model.lazy='sendModal.oldpassword')
|
FormItem(label="您的新密码:",:col="9",right,prop="password",key="password",v-model="rules.password")
input.form-control(type='password',password-strength,name='password',required,v-model.lazy='sendModal.password')
|
FormItem(label="重复新密码:",:col="9",right,prop="confirmPassword",key="confirmPassword",v-model="rules.confirmPassword")
input.form-control(type='password',:matches="sendModal.password",name='confirmPassword',required,v-model.lazy='sendModal.confirmPassword')
|
FormItem(:col="9")
button.btn.btn-sm.btn-info.px-3(type="submit") 下一步
</template>
<script>
const sendef = {
code: '',
oldpassword: '',
password: '',
confirmPassword: ''
}
import { card, cardbody } from '@/components/comp/cards'
import step from '@/components/comp/step'
import icons from '@/components/comp/icon'
import panel from '@/view/layout/panel'
import status from '@/components/comp/status'
import timer from '@/components/plug/timer'
import { form, formItem } from '@/components/comp/form'
export default {
name: 'viewAuth',
components: {
step,
panel,
status,
icons,
Forms: form,
FormItem: formItem,
card,
cardbody,
timer
},
data() {
return {
stepData: ['身份验证', '修改密码', '完成'],
step: 1,
time: 120,
timer: false,
formstate: {},
sendModal: _.cloneDeep(sendef),
rules:{
code:[{
slot:'required',
label:'请输入您收到的验证码'
}],
oldpassword:[{
slot:'required',
label:'请输入您的旧密码'
}],
password:[{
slot:'required',
label:'请输入您的新密码'
},{
slot:'password-strength',
label:' 密码需包含大小写字母数字,且必须大于8位!'
}],
confirmPassword:[{
slot:'required',
label:'请重复您的新密码'
},{
slot:'matches',
label:'确认密码与上一次输入不匹配'
}]
}
}
},
computed:{
getType(){
return this.$route.query.changeBy
}
},
methods: {
userinfo() {
const userinfo = this.$store.state.user.user
this.sendModal.phone = userinfo.phone
this.sendModal.email = userinfo.email
},
getSmsCode() {
api.getSmsCode(this.userData.phone).then(res => {

})
},
next(e) {
this.step =2
this.formstate._reset();
},
sendPassword(e) {
if (!!this.formstate.$valid) {
if (this.step === 1) {
this.step = 2
this.formstate._reset();
}
}
}
},
mounted() {
this.userinfo()
}
}
</script>
<style lang="less">
#authentication {
.step-round > li > a:after {
top: -34%;
height: 3px;
}
}
</style>














? 查看全部
Form 外层组件
<template>
<vue-form v-if="!unform" :class="{'form-inline':inline}" :state="state" v-model="state" @submit.prevent="formMethod" >
<slot></slot>
</vue-form>
<div class="form" v-else>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'vbaForm',
provide() {
return {
vbaForm: this
}
},
props: {
inline: Boolean,
unform: {
type: Boolean,
default: false
},
state: {},
formMethod: Function
},
methods: {
fieldClassName(field) {
if (!field) {
return ''
}
if ((field.$touched || field.$submitted) && field.$valid) {
return 'has-success'
}
if ((field.$touched || field.$submitted) && field.$invalid) {
return 'has-error'
}
}
}
}
</script>
<style lang="less">
@import (reference) '../../../assets/lib/css.less';
form,
div {
.form-group:last-child {
.mgb(0);
}
}
</style>


FormItem
<template lang="pug">
validate.form-group.required-field.row.mb-4(v-if="!!isForm",auto-label,:class='vbaForm.fieldClassName(`${$parent.formstate}[${prop}]`)')
label.col.col-form-label(:class="{'text-right':right}",v-if="!!col||col===12") {{label}}
cols(:col="col",:size="size")
slot
field-messages.help-block.text-danger.position-absolute.mb-0(:name='prop', show='$touched || $submitted',tag="section")
div(v-for="(item,index) in messages",:slot="item.slot") {{item.label}}
rows.form-group(v-else)
label.col.col-form-label(:class="{'text-right':right}",v-if="!!col||col===12") {{label}}
cols.d-flex.align-items-center(:col="col",:size="size")
slot
</template>
<script>
import { rows, cols } from '@/components/layout'
export default {
name: 'formItem',
components: {
rows,
cols
},
inject: ['vbaForm'],
model:{
prop:'messages'
},
props: {
messages:[Array],
label: String,
right: {
type: Boolean,
default: false
},
col: [Number, Array, String],
size: [String, Array],
prop: String,
unForm:{
type:Boolean,
default:false
}
},
data() {
return {}
},
computed: {
isForm() {
return !this.unForm?!this.vbaForm.unform && this.prop:true
}
}
}
</script>


使用案例方法
<template lang="pug">
section#authentication.uc-panel
.d-flex.mt-4
step.w-50.mx-auto(:names="stepData",:current="step")
.d-flex.w-500.mx-auto.my-3
panel.w-100(:stype="step===3?2:3")
Forms(:state="formstate",:formMethod="sendPassword",ref="sendinfo",v-if="step!=3")
template(v-if="step===1")
FormItem(:label="getType==='phone'?'您的手机:':'您的邮箱'",right,:col="9",:unForm="true")
.input-group
input.form-control-plaintext(type="text",readonly,:name="getType",:value="getType==='phone'?$store.state.user.user.phone:$store.state.user.user.email")
span.input-group-btn
timer.btn.btn-sm.btn-secondary(:begin="timer",:second="time",@clevent='getSmsCode')
FormItem(label="您的验证码:",:col="9",right,prop="code",key="code",v-model="rules.code")
input.form-control(type='text',name='code',autocomplete="off",required,v-model.lazy='sendModal.code')
FormItem(:col="9")
button.btn.btn-sm.btn-info.px-3(type="submit") 下一步
template(v-else-if="step===2")
FormItem(label="旧密码:",:col="9",right,prop="oldpassword",key="oldpassword",v-model="rules.oldpassword")
input.form-control(type='password',name='oldpassword',required,v-model.lazy='sendModal.oldpassword')
|
FormItem(label="您的新密码:",:col="9",right,prop="password",key="password",v-model="rules.password")
input.form-control(type='password',password-strength,name='password',required,v-model.lazy='sendModal.password')
|
FormItem(label="重复新密码:",:col="9",right,prop="confirmPassword",key="confirmPassword",v-model="rules.confirmPassword")
input.form-control(type='password',:matches="sendModal.password",name='confirmPassword',required,v-model.lazy='sendModal.confirmPassword')
|
FormItem(:col="9")
button.btn.btn-sm.btn-info.px-3(type="submit") 下一步
</template>
<script>
const sendef = {
code: '',
oldpassword: '',
password: '',
confirmPassword: ''
}
import { card, cardbody } from '@/components/comp/cards'
import step from '@/components/comp/step'
import icons from '@/components/comp/icon'
import panel from '@/view/layout/panel'
import status from '@/components/comp/status'
import timer from '@/components/plug/timer'
import { form, formItem } from '@/components/comp/form'
export default {
name: 'viewAuth',
components: {
step,
panel,
status,
icons,
Forms: form,
FormItem: formItem,
card,
cardbody,
timer
},
data() {
return {
stepData: ['身份验证', '修改密码', '完成'],
step: 1,
time: 120,
timer: false,
formstate: {},
sendModal: _.cloneDeep(sendef),
rules:{
code:[{
slot:'required',
label:'请输入您收到的验证码'
}],
oldpassword:[{
slot:'required',
label:'请输入您的旧密码'
}],
password:[{
slot:'required',
label:'请输入您的新密码'
},{
slot:'password-strength',
label:' 密码需包含大小写字母数字,且必须大于8位!'
}],
confirmPassword:[{
slot:'required',
label:'请重复您的新密码'
},{
slot:'matches',
label:'确认密码与上一次输入不匹配'
}]
}
}
},
computed:{
getType(){
return this.$route.query.changeBy
}
},
methods: {
userinfo() {
const userinfo = this.$store.state.user.user
this.sendModal.phone = userinfo.phone
this.sendModal.email = userinfo.email
},
getSmsCode() {
api.getSmsCode(this.userData.phone).then(res => {

})
},
next(e) {
this.step =2
this.formstate._reset();
},
sendPassword(e) {
if (!!this.formstate.$valid) {
if (this.step === 1) {
this.step = 2
this.formstate._reset();
}
}
}
},
mounted() {
this.userinfo()
}
}
</script>
<style lang="less">
#authentication {
.step-round > li > a:after {
top: -34%;
height: 3px;
}
}
</style>

QQ图片20171222105832.png


QQ图片20171222105842.png


QQ图片20171222105854.png

?

bootstrap4.x+vue2.x 文件上传

VUElopo1983 发表了文章 • 0 个评论 • 1203 次浏览 • 2018-03-16 22:43 • 来自相关话题

template<template>
<div class="upload">
<label class="mb-0" :class="hasSlot ? '' : 'custom-file'" @drop.prevent="onDrop">
<input type="file" :class="hasSlot ? 'd-none' : 'custom-file-input'" @change="onPicker" :multiple="multiple" :accept="accept" v-if="refresh">
<span v-if="hasSlot"><slot></slot></span>
<span v-else class="custom-file-control">选择文件</span>
</label>
<span class="help text-secondary" :class="!!helpblock?`d-block mt-2`:'ml-3'" v-if="help">{{help}}</span>
<cardlayer :class="{'mt-3':files.length}">
<card class="upload-item" v-for="(item,idx) in files" :key="idx" :style="`flex: 0 0 ${1/col*100}%`">
<cardbody>
<div class="upload-cancel">
<div class="icon-mix">
<icon icon="shanchu" @click.native="onCancel(idx)" title="删除"></icon>
<icon icon="reupload" @click.native="upload" v-if="item.stype=='danger'" title="重传"></icon>
</div>
</div>
<div class="upload-preview" :style="{backgroundImage:`url(${item._base64 || item.base64})`}"></div>
<img class="card-img-top" :src="square">
</cardbody>
<cardfooter :class="item.stype!=''&&`bg-${item.stype}`">
<progressbar class="upload-progress" v-bind="item" size="xs"></progressbar>
<div class="d-flex upload-info ">
<div class="text-truncate mb-0 float-left">{{item.name}}</div>
</div>
</cardfooter>
</card>
</cardlayer>
<!--<div class="file-holder">
<div v-for="(item,idx) in files" :key="idx">
<a href="javascript:;" @click="onCancel(idx)">取消</a>
<div class="holder" :style="{backgroundImage:`url(${item.base64})`}">{{ item.name }}</div>
<progressbar v-bind="item" size="xs"></progressbar>
</div>
</div>-->
<button type="button" class="btn btn-ces mt-3" @click="upload" v-if="!autoUpload&&files.length">上传</button>
</div>
</template>javascript<script>
/**
* bootstrap 4.x --> components --> uploader
*
* @param {Boolean} multiple 多选
* @param {Number} max 多选最大值
* @param {Array} maxSize 最大尺寸[width,height] ,不限制不传或者传 0
* @param {Array} allowMime 允许上传的 MIME 类型,默认不限制
* @param {String} url 上传URL
* @param {Boolean} autoUpload 是否选择文件后自动上传
* @param {Function} uploadMethod 自定义上传方法,必须返回Promise,成功resolve
* @param {Function|Object} extraParams
* @param {String} help 上传帮助文字
*
* @event remove 删除
* @event error 错误
* @event success 单个上传成功
* @event completed 全部上传成功
*
* @date 2017-10-24
*
* @requires 依赖 axios icon cards
*
* @version v1.0.0 beta
*
* @author www.bsfans.com 戏子 lopo
*
**/
import icon from './icon'
import progressbar from './progress'
import square from '@/assets/square.png'
import {
cardlayer,
card,
cardheader,
cardbody,
cardfooter
} from '@/components/comp/cards'
import axios from 'axios'

const IMAGE_MIME = ['image/png', 'image/jpg', 'image/gif', 'image/jpeg']

const fnTypeBase64 = text => {
const canvas = document.createElement('canvas')
canvas.width = 200
canvas.height = 200
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'rgba(255,255,255,0.8)'
ctx.fillRect(0, 0, 200, 200)
ctx.stroke()
ctx.font = '40px monaco'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(text, 100, 100)
return canvas.toDataURL('image/jpeg')
}

export default {
name: 'uploader',
props: {
multiple: Boolean,
max: Number,
maxSize: {
type: Array,
default: () =>
},
allowMime: {
type: Array,
default: () =>
},
col: {
type: Number,
default: 6
},
help: String,
helpblock:{
type:Boolean,
default:false
},
url: String,
autoUpload: {
type: Boolean,
default: false
},
uploadMethod: Function,
extraParams: [Function, Object]
},
data() {
return {
files: ,
refresh: true,
square: square
}
},
components: {
progressbar,
icon,
cardlayer,
card,
cardheader,
cardbody,
cardfooter
},
methods: {
onPicked(files) {
if (this.max > 0 && this.files.length + files.length > this.max) {
this.$emit('error','图片太大')
return false
}
const isImage = type => IMAGE_MIME.findIndex(item => item == type) > -1
this.$emit('picked', files)
Array.from(files).forEach(file => {
const o = {
file,
name: file.name,
filesize: file.size,
type: file.name
.substring(file.name.lastIndexOf('.'))
.toLowerCase()
.replace('.', ''),
stype: '',
value: 0,
base64: '',
width: 0,
height: 0,
isimage: isImage(),
completed: false,
uploading: false
}

if (this.allowMime.length) {
if (this.allowMime.findIndex(item => item == file.type) === -1) {
return this.$emit('error')
}
}

const reader = new FileReader()
reader.onload = e => {
o.base64 = e.target.result
const idx = this.files.findIndex(item => {
return item.base64 == o.base64 && item.name == o.name
})
if (idx === -1) {
if (isImage(file.type)) {
const image = new Image()
image.onload = () => {
o.width = image.width
o.height = image.height
const [maxwidth, maxheight] = this.maxSize
if (maxwidth && maxwidth < o.width) {
this.$emit('error')
} else if (maxheight && maxheight < o.height) {
this.$emit('error')
} else {
this.files.push(o)
this.autoUpload && this.fnUpload(o)
}
}
image.src = o.base64
} else {
o._base64 = fnTypeBase64(o.type)
this.autoUpload && this.fnUpload(o)
this.files.push(o)
}
}
}
reader.onerror = () => {
this.$emit('error')
}
reader.readAsDataURL(file)
})
this.reset()
},
reset() {
this.refresh = false
this.$nextTick(() => {
this.refresh = true
})
},
onPicker(e) {
this.onPicked(e.target.files)
},
onDrop(e) {
this.onPicked(e.dataTransfer.files)
},
onCancel(idx) {
this.files.splice(idx, 1)
this.$emit('remove', idx)
},
upload() {
this.files.filter(item => !item.uploading).forEach(this.fnUpload)
},
clearFile() {
this.files =
},
fnUpload(o) {
o.uploading = true
if (this.uploadMethod) {
const p = this.uploadMethod(o)
if (p.then) {
p.then(res => {
o.stype = 'success'
o.value = 100
o.completed = true
this.$emit('success', res)
})
}
} else {
// console.log('no then')
const formData = new FormData()
formData.append('file', o.file)
const params =
typeof this.extraParams == 'function'
? this.extraParams(o)
: this.extraParams
Object.keys(params).forEach(key => formData.append(key, params[key]))
axios
.post(this.url, formData, {
onUploadProgress(evt) {
o.value = 100 * evt.loaded / evt.total
}
})
.then(
res => {
o.stype = 'success'
o.completed = true
this.$emit('success', res)
},
err => {
o.stype = 'danger'
this.$emit('error')
}
)
}
}
},
computed: {
accept() {
return this.allowMime.length ? this.allowMime.join(',') : ''
},
isCompleted() {
if (this.files.length) {
return this.files.filter(item => !item.completed).length == 0
} else {
return false
}
},
hasSlot() {
return !!this.$slots.default
}
},
watch: {
isCompleted(v) {
v && this.$emit('completed')
}
},
mounted() {
const DRAG_EVENTS = ['dragleave', 'drop', 'dragenter', 'dragover']
DRAG_EVENTS.forEach(evtName => {
document.addEventListener(evtName, e => e.preventDefault(), false)
})
}
}
</script>style(less)<style lang="less">
@import (reference) '../../assets/lib/css.less';
.upload {
input[type='file'] {
opacity: 0;
}
.custom-file {
.mgb(1rem);
}
&-progress {
.ps;
top: 0;
left: 0;
right: 0;
}
&-info {
.cp;
align-items: center;
}
/*&-progress {
.ppd;
width:0;
z-index: 2;
.bgc(@ces);
}*/
&-item {
.card-body {
overflow: hidden;
position: relative;
z-index: 1;
}
.card-footer {
.trs;
.pr;
.bgcw;
.pdy(0.3rem);
&[class*='bg'] {
.crw;
}
}
}
&-preview {
.ppd(1.25rem);
.bdr(@cr: rgba(0, 0, 0, 0.1));
background-repeat: no-repeat;
background-position: center center;
background-size: 100%;
}
&-cancel {
.mask;
.trs;
opacity: 0;
.bgc(rgba(0, 0, 0, 0.5));
z-index: 3;
.icon-mix {
.amid;
.iconfont + .iconfont {
.mgl(1.5rem);
}
}
.iconfont {
.fs(1.6rem);
.cp;
.trs;
.trf(scale(0));
.crw;
.trfo(50% 50%);
&:after {
content: '';
.bgc(rgba(0, 0, 0, 0.2));
width: 3rem;
height: 3rem;
.db;
.mask;
.trs;
z-index: -1;
left: -0.7rem;
top: -0.3rem;
.bdrrd(3rem);
}
&:hover:after {
.bgc(@ces);
}
}
.upload .card:hover & {
opacity: 1;
.iconfont {
.trf(scale(1));
}
}
}
}
</style>




? 查看全部

QQ图片20180316224810.png

template
<template>
<div class="upload">
<label class="mb-0" :class="hasSlot ? '' : 'custom-file'" @drop.prevent="onDrop">
<input type="file" :class="hasSlot ? 'd-none' : 'custom-file-input'" @change="onPicker" :multiple="multiple" :accept="accept" v-if="refresh">
<span v-if="hasSlot"><slot></slot></span>
<span v-else class="custom-file-control">选择文件</span>
</label>
<span class="help text-secondary" :class="!!helpblock?`d-block mt-2`:'ml-3'" v-if="help">{{help}}</span>
<cardlayer :class="{'mt-3':files.length}">
<card class="upload-item" v-for="(item,idx) in files" :key="idx" :style="`flex: 0 0 ${1/col*100}%`">
<cardbody>
<div class="upload-cancel">
<div class="icon-mix">
<icon icon="shanchu" @click.native="onCancel(idx)" title="删除"></icon>
<icon icon="reupload" @click.native="upload" v-if="item.stype=='danger'" title="重传"></icon>
</div>
</div>
<div class="upload-preview" :style="{backgroundImage:`url(${item._base64 || item.base64})`}"></div>
<img class="card-img-top" :src="square">
</cardbody>
<cardfooter :class="item.stype!=''&&`bg-${item.stype}`">
<progressbar class="upload-progress" v-bind="item" size="xs"></progressbar>
<div class="d-flex upload-info ">
<div class="text-truncate mb-0 float-left">{{item.name}}</div>
</div>
</cardfooter>
</card>
</cardlayer>
<!--<div class="file-holder">
<div v-for="(item,idx) in files" :key="idx">
<a href="javascript:;" @click="onCancel(idx)">取消</a>
<div class="holder" :style="{backgroundImage:`url(${item.base64})`}">{{ item.name }}</div>
<progressbar v-bind="item" size="xs"></progressbar>
</div>
</div>-->
<button type="button" class="btn btn-ces mt-3" @click="upload" v-if="!autoUpload&&files.length">上传</button>
</div>
</template>
javascript
<script>
/**
* bootstrap 4.x --> components --> uploader
*
* @param {Boolean} multiple 多选
* @param {Number} max 多选最大值
* @param {Array} maxSize 最大尺寸[width,height] ,不限制不传或者传 0
* @param {Array} allowMime 允许上传的 MIME 类型,默认不限制
* @param {String} url 上传URL
* @param {Boolean} autoUpload 是否选择文件后自动上传
* @param {Function} uploadMethod 自定义上传方法,必须返回Promise,成功resolve
* @param {Function|Object} extraParams
* @param {String} help 上传帮助文字
*
* @event remove 删除
* @event error 错误
* @event success 单个上传成功
* @event completed 全部上传成功
*
* @date 2017-10-24
*
* @requires 依赖 axios icon cards
*
* @version v1.0.0 beta
*
* @author www.bsfans.com 戏子 lopo
*
**/
import icon from './icon'
import progressbar from './progress'
import square from '@/assets/square.png'
import {
cardlayer,
card,
cardheader,
cardbody,
cardfooter
} from '@/components/comp/cards'
import axios from 'axios'

const IMAGE_MIME = ['image/png', 'image/jpg', 'image/gif', 'image/jpeg']

const fnTypeBase64 = text => {
const canvas = document.createElement('canvas')
canvas.width = 200
canvas.height = 200
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'rgba(255,255,255,0.8)'
ctx.fillRect(0, 0, 200, 200)
ctx.stroke()
ctx.font = '40px monaco'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(text, 100, 100)
return canvas.toDataURL('image/jpeg')
}

export default {
name: 'uploader',
props: {
multiple: Boolean,
max: Number,
maxSize: {
type: Array,
default: () =>
},
allowMime: {
type: Array,
default: () =>
},
col: {
type: Number,
default: 6
},
help: String,
helpblock:{
type:Boolean,
default:false
},
url: String,
autoUpload: {
type: Boolean,
default: false
},
uploadMethod: Function,
extraParams: [Function, Object]
},
data() {
return {
files: ,
refresh: true,
square: square
}
},
components: {
progressbar,
icon,
cardlayer,
card,
cardheader,
cardbody,
cardfooter
},
methods: {
onPicked(files) {
if (this.max > 0 && this.files.length + files.length > this.max) {
this.$emit('error','图片太大')
return false
}
const isImage = type => IMAGE_MIME.findIndex(item => item == type) > -1
this.$emit('picked', files)
Array.from(files).forEach(file => {
const o = {
file,
name: file.name,
filesize: file.size,
type: file.name
.substring(file.name.lastIndexOf('.'))
.toLowerCase()
.replace('.', ''),
stype: '',
value: 0,
base64: '',
width: 0,
height: 0,
isimage: isImage(),
completed: false,
uploading: false
}

if (this.allowMime.length) {
if (this.allowMime.findIndex(item => item == file.type) === -1) {
return this.$emit('error')
}
}

const reader = new FileReader()
reader.onload = e => {
o.base64 = e.target.result
const idx = this.files.findIndex(item => {
return item.base64 == o.base64 && item.name == o.name
})
if (idx === -1) {
if (isImage(file.type)) {
const image = new Image()
image.onload = () => {
o.width = image.width
o.height = image.height
const [maxwidth, maxheight] = this.maxSize
if (maxwidth && maxwidth < o.width) {
this.$emit('error')
} else if (maxheight && maxheight < o.height) {
this.$emit('error')
} else {
this.files.push(o)
this.autoUpload && this.fnUpload(o)
}
}
image.src = o.base64
} else {
o._base64 = fnTypeBase64(o.type)
this.autoUpload && this.fnUpload(o)
this.files.push(o)
}
}
}
reader.onerror = () => {
this.$emit('error')
}
reader.readAsDataURL(file)
})
this.reset()
},
reset() {
this.refresh = false
this.$nextTick(() => {
this.refresh = true
})
},
onPicker(e) {
this.onPicked(e.target.files)
},
onDrop(e) {
this.onPicked(e.dataTransfer.files)
},
onCancel(idx) {
this.files.splice(idx, 1)
this.$emit('remove', idx)
},
upload() {
this.files.filter(item => !item.uploading).forEach(this.fnUpload)
},
clearFile() {
this.files =
},
fnUpload(o) {
o.uploading = true
if (this.uploadMethod) {
const p = this.uploadMethod(o)
if (p.then) {
p.then(res => {
o.stype = 'success'
o.value = 100
o.completed = true
this.$emit('success', res)
})
}
} else {
// console.log('no then')
const formData = new FormData()
formData.append('file', o.file)
const params =
typeof this.extraParams == 'function'
? this.extraParams(o)
: this.extraParams
Object.keys(params).forEach(key => formData.append(key, params[key]))
axios
.post(this.url, formData, {
onUploadProgress(evt) {
o.value = 100 * evt.loaded / evt.total
}
})
.then(
res => {
o.stype = 'success'
o.completed = true
this.$emit('success', res)
},
err => {
o.stype = 'danger'
this.$emit('error')
}
)
}
}
},
computed: {
accept() {
return this.allowMime.length ? this.allowMime.join(',') : ''
},
isCompleted() {
if (this.files.length) {
return this.files.filter(item => !item.completed).length == 0
} else {
return false
}
},
hasSlot() {
return !!this.$slots.default
}
},
watch: {
isCompleted(v) {
v && this.$emit('completed')
}
},
mounted() {
const DRAG_EVENTS = ['dragleave', 'drop', 'dragenter', 'dragover']
DRAG_EVENTS.forEach(evtName => {
document.addEventListener(evtName, e => e.preventDefault(), false)
})
}
}
</script>
style(less)
<style lang="less">
@import (reference) '../../assets/lib/css.less';
.upload {
input[type='file'] {
opacity: 0;
}
.custom-file {
.mgb(1rem);
}
&-progress {
.ps;
top: 0;
left: 0;
right: 0;
}
&-info {
.cp;
align-items: center;
}
/*&-progress {
.ppd;
width:0;
z-index: 2;
.bgc(@ces);
}*/
&-item {
.card-body {
overflow: hidden;
position: relative;
z-index: 1;
}
.card-footer {
.trs;
.pr;
.bgcw;
.pdy(0.3rem);
&[class*='bg'] {
.crw;
}
}
}
&-preview {
.ppd(1.25rem);
.bdr(@cr: rgba(0, 0, 0, 0.1));
background-repeat: no-repeat;
background-position: center center;
background-size: 100%;
}
&-cancel {
.mask;
.trs;
opacity: 0;
.bgc(rgba(0, 0, 0, 0.5));
z-index: 3;
.icon-mix {
.amid;
.iconfont + .iconfont {
.mgl(1.5rem);
}
}
.iconfont {
.fs(1.6rem);
.cp;
.trs;
.trf(scale(0));
.crw;
.trfo(50% 50%);
&:after {
content: '';
.bgc(rgba(0, 0, 0, 0.2));
width: 3rem;
height: 3rem;
.db;
.mask;
.trs;
z-index: -1;
left: -0.7rem;
top: -0.3rem;
.bdrrd(3rem);
}
&:hover:after {
.bgc(@ces);
}
}
.upload .card:hover & {
opacity: 1;
.iconfont {
.trf(scale(1));
}
}
}
}
</style>

QQ图片20180316224810.png

?

vue+bootstrap4+tooltip.js 实现简单的tooltip

VUElopo1983 发表了文章 • 0 个评论 • 1976 次浏览 • 2018-01-24 17:33 • 来自相关话题

<template lang="pug">
button(:class="`btn btn-${size} btn-${stype}`",ref="button",@mouseover="initPopper")
<slot></slot>
<template v-if="!$slots.default">{{label}}</template>
</template>
<script>
import lib from '@/utils/lib'
import btn from '@/components/comp/button'
import Tooltip from 'tooltip.js'
export default {
name: 'vbaToolTip',
components: {
btn
},
props: {
label: String,
size: String,
stype: String,
placement: {
type: String,
default: 'top'
},
html: {
type: Boolean,
default: false
},
content: String
},
data() {
return {
popperInstance: null
}
},
methods: {
initPopper() {
if (!this.popperInstance) {
const vm = this
this.popperInstance = new Tooltip(this.$refs.button, {
placement: `${this.placement}`,
template: `<div class="tooltip bs-tooltip-${
this.placement
}" role="tooltip">
<div class="tooltip-arrow arrow"></div>
<div class="tooltip-inner">
</div>
</div>`,
title: this.content,
html:this.html,
contaier:document.getElementsByTagName('body'),
onCreate() {
vm.$emit('on-create', this.popperInstance)
},
onUpdate() {
vm.$emit('on-update', this.popperInstance)
}
})
}
}
}
}
</script>
<style lang="less">
.tooltip {
opacity: 1 !important;
}
</style> 查看全部
<template lang="pug">
button(:class="`btn btn-${size} btn-${stype}`",ref="button",@mouseover="initPopper")
<slot></slot>
<template v-if="!$slots.default">{{label}}</template>
</template>
<script>
import lib from '@/utils/lib'
import btn from '@/components/comp/button'
import Tooltip from 'tooltip.js'
export default {
name: 'vbaToolTip',
components: {
btn
},
props: {
label: String,
size: String,
stype: String,
placement: {
type: String,
default: 'top'
},
html: {
type: Boolean,
default: false
},
content: String
},
data() {
return {
popperInstance: null
}
},
methods: {
initPopper() {
if (!this.popperInstance) {
const vm = this
this.popperInstance = new Tooltip(this.$refs.button, {
placement: `${this.placement}`,
template: `<div class="tooltip bs-tooltip-${
this.placement
}" role="tooltip">
<div class="tooltip-arrow arrow"></div>
<div class="tooltip-inner">
</div>
</div>`,
title: this.content,
html:this.html,
contaier:document.getElementsByTagName('body'),
onCreate() {
vm.$emit('on-create', this.popperInstance)
},
onUpdate() {
vm.$emit('on-update', this.popperInstance)
}
})
}
}
}
}
</script>
<style lang="less">
.tooltip {
opacity: 1 !important;
}
</style>

vue-form + bootstrap4 beta 二次封装

VUElopo1983 发表了文章 • 1 个评论 • 1285 次浏览 • 2017-12-22 10:54 • 来自相关话题

Form 外层组件<template>
<vue-form v-if="!unform" :class="{'form-inline':inline}" :state="state" v-model="state" @submit.prevent="formMethod" >
<slot></slot>
</vue-form>
<div class="form" v-else>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'vbaForm',
provide() {
return {
vbaForm: this
}
},
props: {
inline: Boolean,
unform: {
type: Boolean,
default: false
},
state: {},
formMethod: Function
},
methods: {
fieldClassName(field) {
if (!field) {
return ''
}
if ((field.$touched || field.$submitted) && field.$valid) {
return 'has-success'
}
if ((field.$touched || field.$submitted) && field.$invalid) {
return 'has-error'
}
}
}
}
</script>
<style lang="less">
@import (reference) '../../../assets/lib/css.less';
form,
div {
.form-group:last-child {
.mgb(0);
}
}
</style>


FormItem<template lang="pug">
validate.form-group.required-field.row.mb-4(v-if="!!isForm",auto-label,:class='vbaForm.fieldClassName(`${$parent.formstate}[${prop}]`)')
label.col.col-form-label(:class="{'text-right':right}",v-if="!!col||col===12") {{label}}
cols(:col="col",:size="size")
slot
field-messages.help-block.text-danger.position-absolute.mb-0(:name='prop', show='$touched || $submitted',tag="section")
div(v-for="(item,index) in messages",:slot="item.slot") {{item.label}}
rows.form-group(v-else)
label.col.col-form-label(:class="{'text-right':right}",v-if="!!col||col===12") {{label}}
cols.d-flex.align-items-center(:col="col",:size="size")
slot
</template>
<script>
import { rows, cols } from '@/components/layout'
export default {
name: 'formItem',
components: {
rows,
cols
},
inject: ['vbaForm'],
model:{
prop:'messages'
},
props: {
messages:[Array],
label: String,
right: {
type: Boolean,
default: false
},
col: [Number, Array, String],
size: [String, Array],
prop: String,
unForm:{
type:Boolean,
default:false
}
},
data() {
return {}
},
computed: {
isForm() {
return !this.unForm?!this.vbaForm.unform && this.prop:true
}
}
}
</script>


使用案例方法<template lang="pug">
section#authentication.uc-panel
.d-flex.mt-4
step.w-50.mx-auto(:names="stepData",:current="step")
.d-flex.w-500.mx-auto.my-3
panel.w-100(:stype="step===3?2:3")
Forms(:state="formstate",:formMethod="sendPassword",ref="sendinfo",v-if="step!=3")
template(v-if="step===1")
FormItem(:label="getType==='phone'?'您的手机:':'您的邮箱'",right,:col="9",:unForm="true")
.input-group
input.form-control-plaintext(type="text",readonly,:name="getType",:value="getType==='phone'?$store.state.user.user.phone:$store.state.user.user.email")
span.input-group-btn
timer.btn.btn-sm.btn-secondary(:begin="timer",:second="time",@clevent='getSmsCode')
FormItem(label="您的验证码:",:col="9",right,prop="code",key="code",v-model="rules.code")
input.form-control(type='text',name='code',autocomplete="off",required,v-model.lazy='sendModal.code')
FormItem(:col="9")
button.btn.btn-sm.btn-info.px-3(type="submit") 下一步
template(v-else-if="step===2")
FormItem(label="旧密码:",:col="9",right,prop="oldpassword",key="oldpassword",v-model="rules.oldpassword")
input.form-control(type='password',name='oldpassword',required,v-model.lazy='sendModal.oldpassword')
|
FormItem(label="您的新密码:",:col="9",right,prop="password",key="password",v-model="rules.password")
input.form-control(type='password',password-strength,name='password',required,v-model.lazy='sendModal.password')
|
FormItem(label="重复新密码:",:col="9",right,prop="confirmPassword",key="confirmPassword",v-model="rules.confirmPassword")
input.form-control(type='password',:matches="sendModal.password",name='confirmPassword',required,v-model.lazy='sendModal.confirmPassword')
|
FormItem(:col="9")
button.btn.btn-sm.btn-info.px-3(type="submit") 下一步
</template>
<script>
const sendef = {
code: '',
oldpassword: '',
password: '',
confirmPassword: ''
}
import { card, cardbody } from '@/components/comp/cards'
import step from '@/components/comp/step'
import icons from '@/components/comp/icon'
import panel from '@/view/layout/panel'
import status from '@/components/comp/status'
import timer from '@/components/plug/timer'
import { form, formItem } from '@/components/comp/form'
export default {
name: 'viewAuth',
components: {
step,
panel,
status,
icons,
Forms: form,
FormItem: formItem,
card,
cardbody,
timer
},
data() {
return {
stepData: ['身份验证', '修改密码', '完成'],
step: 1,
time: 120,
timer: false,
formstate: {},
sendModal: _.cloneDeep(sendef),
rules:{
code:[{
slot:'required',
label:'请输入您收到的验证码'
}],
oldpassword:[{
slot:'required',
label:'请输入您的旧密码'
}],
password:[{
slot:'required',
label:'请输入您的新密码'
},{
slot:'password-strength',
label:' 密码需包含大小写字母数字,且必须大于8位!'
}],
confirmPassword:[{
slot:'required',
label:'请重复您的新密码'
},{
slot:'matches',
label:'确认密码与上一次输入不匹配'
}]
}
}
},
computed:{
getType(){
return this.$route.query.changeBy
}
},
methods: {
userinfo() {
const userinfo = this.$store.state.user.user
this.sendModal.phone = userinfo.phone
this.sendModal.email = userinfo.email
},
getSmsCode() {
api.getSmsCode(this.userData.phone).then(res => {

})
},
next(e) {
this.step =2
this.formstate._reset();
},
sendPassword(e) {
if (!!this.formstate.$valid) {
if (this.step === 1) {
this.step = 2
this.formstate._reset();
}
}
}
},
mounted() {
this.userinfo()
}
}
</script>
<style lang="less">
#authentication {
.step-round > li > a:after {
top: -34%;
height: 3px;
}
}
</style>














? 查看全部
Form 外层组件
<template>
<vue-form v-if="!unform" :class="{'form-inline':inline}" :state="state" v-model="state" @submit.prevent="formMethod" >
<slot></slot>
</vue-form>
<div class="form" v-else>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'vbaForm',
provide() {
return {
vbaForm: this
}
},
props: {
inline: Boolean,
unform: {
type: Boolean,
default: false
},
state: {},
formMethod: Function
},
methods: {
fieldClassName(field) {
if (!field) {
return ''
}
if ((field.$touched || field.$submitted) && field.$valid) {
return 'has-success'
}
if ((field.$touched || field.$submitted) && field.$invalid) {
return 'has-error'
}
}
}
}
</script>
<style lang="less">
@import (reference) '../../../assets/lib/css.less';
form,
div {
.form-group:last-child {
.mgb(0);
}
}
</style>


FormItem
<template lang="pug">
validate.form-group.required-field.row.mb-4(v-if="!!isForm",auto-label,:class='vbaForm.fieldClassName(`${$parent.formstate}[${prop}]`)')
label.col.col-form-label(:class="{'text-right':right}",v-if="!!col||col===12") {{label}}
cols(:col="col",:size="size")
slot
field-messages.help-block.text-danger.position-absolute.mb-0(:name='prop', show='$touched || $submitted',tag="section")
div(v-for="(item,index) in messages",:slot="item.slot") {{item.label}}
rows.form-group(v-else)
label.col.col-form-label(:class="{'text-right':right}",v-if="!!col||col===12") {{label}}
cols.d-flex.align-items-center(:col="col",:size="size")
slot
</template>
<script>
import { rows, cols } from '@/components/layout'
export default {
name: 'formItem',
components: {
rows,
cols
},
inject: ['vbaForm'],
model:{
prop:'messages'
},
props: {
messages:[Array],
label: String,
right: {
type: Boolean,
default: false
},
col: [Number, Array, String],
size: [String, Array],
prop: String,
unForm:{
type:Boolean,
default:false
}
},
data() {
return {}
},
computed: {
isForm() {
return !this.unForm?!this.vbaForm.unform && this.prop:true
}
}
}
</script>


使用案例方法
<template lang="pug">
section#authentication.uc-panel
.d-flex.mt-4
step.w-50.mx-auto(:names="stepData",:current="step")
.d-flex.w-500.mx-auto.my-3
panel.w-100(:stype="step===3?2:3")
Forms(:state="formstate",:formMethod="sendPassword",ref="sendinfo",v-if="step!=3")
template(v-if="step===1")
FormItem(:label="getType==='phone'?'您的手机:':'您的邮箱'",right,:col="9",:unForm="true")
.input-group
input.form-control-plaintext(type="text",readonly,:name="getType",:value="getType==='phone'?$store.state.user.user.phone:$store.state.user.user.email")
span.input-group-btn
timer.btn.btn-sm.btn-secondary(:begin="timer",:second="time",@clevent='getSmsCode')
FormItem(label="您的验证码:",:col="9",right,prop="code",key="code",v-model="rules.code")
input.form-control(type='text',name='code',autocomplete="off",required,v-model.lazy='sendModal.code')
FormItem(:col="9")
button.btn.btn-sm.btn-info.px-3(type="submit") 下一步
template(v-else-if="step===2")
FormItem(label="旧密码:",:col="9",right,prop="oldpassword",key="oldpassword",v-model="rules.oldpassword")
input.form-control(type='password',name='oldpassword',required,v-model.lazy='sendModal.oldpassword')
|
FormItem(label="您的新密码:",:col="9",right,prop="password",key="password",v-model="rules.password")
input.form-control(type='password',password-strength,name='password',required,v-model.lazy='sendModal.password')
|
FormItem(label="重复新密码:",:col="9",right,prop="confirmPassword",key="confirmPassword",v-model="rules.confirmPassword")
input.form-control(type='password',:matches="sendModal.password",name='confirmPassword',required,v-model.lazy='sendModal.confirmPassword')
|
FormItem(:col="9")
button.btn.btn-sm.btn-info.px-3(type="submit") 下一步
</template>
<script>
const sendef = {
code: '',
oldpassword: '',
password: '',
confirmPassword: ''
}
import { card, cardbody } from '@/components/comp/cards'
import step from '@/components/comp/step'
import icons from '@/components/comp/icon'
import panel from '@/view/layout/panel'
import status from '@/components/comp/status'
import timer from '@/components/plug/timer'
import { form, formItem } from '@/components/comp/form'
export default {
name: 'viewAuth',
components: {
step,
panel,
status,
icons,
Forms: form,
FormItem: formItem,
card,
cardbody,
timer
},
data() {
return {
stepData: ['身份验证', '修改密码', '完成'],
step: 1,
time: 120,
timer: false,
formstate: {},
sendModal: _.cloneDeep(sendef),
rules:{
code:[{
slot:'required',
label:'请输入您收到的验证码'
}],
oldpassword:[{
slot:'required',
label:'请输入您的旧密码'
}],
password:[{
slot:'required',
label:'请输入您的新密码'
},{
slot:'password-strength',
label:' 密码需包含大小写字母数字,且必须大于8位!'
}],
confirmPassword:[{
slot:'required',
label:'请重复您的新密码'
},{
slot:'matches',
label:'确认密码与上一次输入不匹配'
}]
}
}
},
computed:{
getType(){
return this.$route.query.changeBy
}
},
methods: {
userinfo() {
const userinfo = this.$store.state.user.user
this.sendModal.phone = userinfo.phone
this.sendModal.email = userinfo.email
},
getSmsCode() {
api.getSmsCode(this.userData.phone).then(res => {

})
},
next(e) {
this.step =2
this.formstate._reset();
},
sendPassword(e) {
if (!!this.formstate.$valid) {
if (this.step === 1) {
this.step = 2
this.formstate._reset();
}
}
}
},
mounted() {
this.userinfo()
}
}
</script>
<style lang="less">
#authentication {
.step-round > li > a:after {
top: -34%;
height: 3px;
}
}
</style>

QQ图片20171222105832.png


QQ图片20171222105842.png


QQ图片20171222105854.png

?