feat(upload): commit as a checkpoint, wip

This commit is contained in:
07akioni 2020-02-17 15:36:36 +08:00
parent 1c497bf441
commit 2e35d3f97e
7 changed files with 330 additions and 81 deletions

View File

@ -1,6 +1,8 @@
# 基础用法
```html
<n-upload action="http://localhost:3000/upload-test">
<n-button>上传文件</n-button>
</n-upload>
<div style="overflow: hidden">
<n-upload action="http://localhost:3000/upload-test">
<n-button>上传文件</n-button>
</n-upload>
</div>
```

View File

@ -2,14 +2,30 @@ const express = require('express')
const multer = require('multer')
const cors = require('cors')
const path = require('path')
const fs = require('fs')
const app = express()
const upload = multer({ dest: path.resolve(__dirname, 'temp') })
const dest = path.resolve(__dirname, 'temp')
const upload = multer({ dest })
app.options('/upload-test', cors())
app.post('/upload-test', cors(), upload.any(), function (req, res, next) {
console.log(req.files)
res.send('very good')
})
app.post(
'/upload-test',
cors(),
function (req, res, next) {
req.on('close', () => {
console.log('文件上传取消')
})
req.on('error', () => {
console.log('文件上传出错')
})
next()
},
upload.any(),
function (req, res, next) {
if (!fs.existsSync(dest)) fs.mkdirSync(dest)
res.send('very good')
}
)
app.listen(3000)

View File

@ -18,70 +18,45 @@
</div>
<div class="n-upload-file-list">
<template v-for="(file, index) in fileList">
<div
v-if="!isFileRemoved(file)"
<n-upload-file
:key="index"
class="n-upload-file"
:class="{
[`n-upload-file--${getProgressStatus(file)}-status`]: true
}"
>
<div class="n-upload-file-info">
<div class="n-upload-file-info__name">
{{ file.name }}
</div>
<div class="n-upload-file-info__action">
<n-button circle size="tiny">
<template v-slot:icon>
<close-outline />
</template>
</n-button>
<n-button circle size="tiny">
<template v-slot:icon>
<download-outline />
</template>
</n-button>
</div>
</div>
<n-upload-progress
:show="!isFileUploaded(file)"
:percentage="file.percentage"
:status="getProgressStatus(file)"
/>
</div>
:file="file"
:index="index"
@download-click="handleDownloadClick"
@cancel-click="handleCancelClick"
@remove-click="handleRemoveClick"
/>
</template>
</div>
</div>
</template>
<script>
import NButton from '../../Button'
import closeOutline from '../../_icons/close-outline'
import downloadOutline from '../../_icons/download-outline'
import withapp from '../../_mixins/withapp'
import themeable from '../../_mixins/themeable'
import NUploadProgress from './UploadProgress'
import NUploadFile from './UploadFile'
function XHRHandlers (componentInstance, index) {
function XHRHandlers (componentInstance, fileIndex) {
const file = componentInstance.fileList[fileIndex]
const formDataList = componentInstance.formDataList
const XHRs = componentInstance.XHRs
return {
handleXHRLoad (e) {
const file = componentInstance.fileList[index]
file.status = 'done'
file.percentage = 100
console.log('!!!done')
XHRs[fileIndex] = null
formDataList[fileIndex] = null
},
handleXHRAbort (e) {
componentInstance.fileList[index].status = 'removed'
console.log('!!!abort')
file.status = 'removed'
XHRs[fileIndex] = null
formDataList[fileIndex] = null
},
handleXHRError (e) {
componentInstance.fileList[index].status = 'error'
console.log('!!!error')
file.status = 'error'
},
handleXHRProgress (e) {
console.log('!!!progress')
if (e.lengthComputable) {
const file = componentInstance.fileList[index]
const progress = Math.ceil((e.loaded / e.total) * 100)
file.percentage = progress
}
@ -89,8 +64,8 @@ function XHRHandlers (componentInstance, index) {
}
}
function registerHandler (request, componentInstance, index) {
const handlers = XHRHandlers(componentInstance, index)
function registerHandler (componentInstance, fileIndex, request) {
const handlers = XHRHandlers(componentInstance, fileIndex)
request.onabort = handlers.handleXHRAbort
request.onerror = handlers.handleXHRError
request.onload = handlers.handleXHRLoad
@ -99,20 +74,59 @@ function registerHandler (request, componentInstance, index) {
}
}
function submit (method = 'POST', action, formData, index, componentInstance) {
const request = new XMLHttpRequest()
registerHandler(request, componentInstance, index)
request.open(method, action)
request.send(formData)
function setHeaders (request, headers) {
if (!headers) return
Object.keys(headers).forEach(key => {
request.setRequestHeader(request, headers[key])
})
}
function unwrapData (data) {
if (typeof data === 'function') {
return data()
}
return data
}
function appendData (formData, data) {
const dataObject = unwrapData(data)
if (!dataObject) return
Object.keys(dataObject).forEach(key => {
formData.append(key, dataObject[key])
})
}
function submit (
componentInstance,
fileIndex,
{
method,
action,
withCredentials,
headers,
data,
formData
}
) {
const request = new XMLHttpRequest()
componentInstance.XHRs[fileIndex] = request
request.withCredentials = withCredentials
setHeaders(request, headers)
appendData(formData, data)
registerHandler(componentInstance, fileIndex, request)
request.open(method, action)
request.send(formData)
const file = componentInstance.fileList[fileIndex]
file.status = 'uploading'
}
/**
* fils status ['pending', 'uploading', 'done', 'removed', 'error']
*/
export default {
name: 'NUpload',
components: {
NButton,
NUploadProgress,
closeOutline,
downloadOutline
NUploadFile
},
mixins: [ withapp, themeable ],
props: {
@ -140,6 +154,10 @@ export default {
type: Boolean,
default: false
},
data: {
type: [Boolean, Function],
default: null
},
headers: {
type: Object,
default: null
@ -155,12 +173,25 @@ export default {
onChange: {
type: Function,
default: () => {}
},
onRemove: {
type: Function,
default: () => true
},
onDownload: {
type: Function,
default: () => true
},
defaultUpload: {
type: Boolean,
default: true
}
},
data () {
return {
fileList: [],
formDataList: []
formDataList: [],
XHRs: []
}
},
methods: {
@ -168,22 +199,10 @@ export default {
if (this.disabled) return
this.$refs.input.click()
},
isFileRemoved (file) {
return file.status === 'removed'
},
isFileUploaded (file) {
return file.status === 'done'
},
getProgressStatus (file) {
if (file.status === 'done') return 'success'
if (file.status === 'error') return 'error'
return 'info'
},
handleFileInputChange (e) {
const fileList = this.fileList
const formDataList = this.formDataList
const fieldName = this.name
const memorizedFileListLength = fileList.length
Array.from(e.target.files).forEach((file, index) => {
fileList.push({
name: file.name,
@ -193,12 +212,56 @@ export default {
const formData = new FormData()
formData.append(fieldName, file)
formDataList.push(formData)
this.submit(formData, memorizedFileListLength + index)
})
if (this.defaultUpload) {
this.submit()
}
e.target.value = null
},
submit (formData, index) {
submit(this.method, this.action, formData, index, this)
submit () {
this.fileList.forEach((file, fileIndex) => {
const formData = this.formDataList[fileIndex]
if (file.status === 'pending') {
submit(
this,
fileIndex,
{
method: this.method,
action: this.action,
withCredentials: this.withCredentials,
headers: this.headers,
data: this.data,
formData
}
)
}
})
},
handleRemoveClick (file, fileIndex) {
Promise.resolve(
this.onRemove(file)
).then(
res => {
if (res === true) {
this.fileList[fileIndex].status = 'removed'
this.XHRs[fileIndex] = null
this.formDataList[fileIndex] = null
}
}
)
},
handleDownloadClick (file, fileIndex) {
Promise.resolve(
this.onDownload(file)
).then(
res => {
if (res === true) {} // do something
}
)
},
handleCancelClick (file, fileIndex) {
const XHR = this.XHRs[fileIndex]
XHR.abort()
}
}
}

View File

@ -0,0 +1,130 @@
<template>
<n-fade-in-height-expand-transition>
<div
v-if="!isFileRemoved"
class="n-upload-file"
:class="{
[`n-upload-file--${progressStatus}-status`]: true
}"
>
<div class="n-upload-file-info">
<div class="n-upload-file-info__name">
<n-icon>
<attach-outline />
</n-icon> {{ file.name }}
</div>
<div class="n-upload-file-info__action">
<n-button
v-if="showRemoveButton || showCancelButton"
key="closeOrTrash"
circle
size="tiny"
@click="handleRemoveOrCancelClick"
>
<template v-slot:icon>
<n-icon-switch-transition>
<trash-outline v-if="showRemoveButton" key="trash" />
<close-outline v-else key="close" />
</n-icon-switch-transition>
</template>
</n-button>
<n-button
v-if="showDownloadButton"
key="download"
circle
size="tiny"
@click="handleDownloadClick"
>
<template v-slot:icon>
<download-outline />
</template>
</n-button>
</div>
</div>
<n-upload-progress
:show="!isFileUploaded"
:percentage="file.percentage"
:status="progressStatus"
/>
</div>
</n-fade-in-height-expand-transition>
</template>
<script>
import NButton from '../../Button'
import closeOutline from '../../_icons/close-outline'
import downloadOutline from '../../_icons/download-outline'
import trashOutline from '../../_icons/trash-outline'
import NUploadProgress from './UploadProgress'
import NFadeInHeightExpandTransition from '../../_transition/FadeInHeightExpandTransition'
import attachOutline from '../../_icons/attach-outline'
import NIcon from '../../Icon'
import NIconSwitchTransition from '../../_transition/IconSwitchTransition'
export default {
name: 'NUploadFile',
components: {
NButton,
NUploadProgress,
attachOutline,
closeOutline,
NIcon,
downloadOutline,
trashOutline,
NFadeInHeightExpandTransition,
NIconSwitchTransition
},
props: {
file: {
type: Object,
required: true
},
index: {
type: Number,
required: true
}
},
computed: {
progressStatus () {
const file = this.file
if (file.status === 'done') return 'success'
if (file.status === 'error') return 'error'
return 'info'
},
isFileRemoved () {
const file = this.file
return file.status === 'removed'
},
isFileUploaded () {
const file = this.file
return file.status === 'done'
},
showCancelButton () {
const file = this.file
return ['uploading', 'pending', 'error'].includes(file.status)
},
showRemoveButton () {
const file = this.file
return ['done'].includes(file.status)
},
showDownloadButton () {
const file = this.file
return ['done'].includes(file.status)
}
},
methods: {
handleRemoveOrCancelClick () {
if (this.showRemoveButton) {
this.$emit('remove-click', this.file, this.index)
} else if (this.showCancelButton) {
this.$emit('cancel-click', this.file, this.index)
} else {
console.error('[naive-ui/upload]: the button clicked type is unknown.')
}
},
handleDownloadClick () {
this.$emit('download-click', this.file, this.index)
}
}
}
</script>

View File

@ -14,8 +14,10 @@
margin-top: 8px;
line-height: 1.5;
@include b(upload-file) {
@include fade-in-height-expand-transition;
box-sizing: border-box;
cursor: pointer;
padding: 6px 12px 0 12px;
padding: 0px 12px 0 6px;
transition: background-color .3s $--n-ease-in-out-cubic-bezier;
border-radius: 6px;
&:hover {
@ -42,13 +44,24 @@
}
@include b(upload-file-info) {
position: relative;
padding-top: 6px;
padding-bottom: 6px;
@include e(name) {
text-overflow: ellipsis;
overflow: hidden;
@include b(icon) {
font-size: 18px;
margin-right: 2px;
fill: $--n-meta-text-color;
stroke: $--n-meta-text-color;
vertical-align: middle;
}
font-size: 14px;
color: $--n-secondary-text-color;
transition: color .3s $--n-ease-in-out-cubic-bezier;
}
@include e(action) {
padding-top: inherit;
padding-bottom: inherit;
position: absolute;
right: 0;
@ -59,15 +72,23 @@
align-items: center;
transition: opacity .2s $--n-ease-in-out-cubic-bezier;
flex-direction: row-reverse;
// opacity: 0;
@include b(button) {
&:not(:first-child) {
margin-right: 8px;
}
@include b(icon) {
svg {
@include icon-switch-transition;
}
}
}
}
}
@include b(progress) {
@include fade-in-height-expand-transition;
@include fade-in-height-expand-transition($fold-padding: true);
box-sizing: border-box;
padding-bottom: 6px;
margin-bottom: 6px;
}
}

View File

@ -135,12 +135,22 @@
}
}
@mixin fade-in-height-expand-transition ($name: 'fade-in-height-expand', $duration: .3s, $original-transition: (), $leave-delay: 0s) {
@mixin fade-in-height-expand-transition (
$name: 'fade-in-height-expand',
$duration: .3s,
$original-transition: (),
$leave-delay: 0s,
$fold-padding: false
) {
&.#{$namespace}-#{$name}-transition-leave, &.#{$namespace}-#{$name}-transition-enter-to {
opacity: 1;
}
&.#{$namespace}-#{$name}-transition-leave-to, &.#{$namespace}-#{$name}-transition-enter {
opacity: 0;
@if $fold-padding {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
margin-top: 0 !important;
margin-bottom: 0 !important;
}
@ -151,6 +161,8 @@
opacity $duration $--n-ease-out-cubic-bezier $leave-delay,
margin-top $duration $--n-ease-in-out-cubic-bezier $leave-delay,
margin-bottom $duration $--n-ease-in-out-cubic-bezier $leave-delay,
padding-top $duration $--n-ease-in-out-cubic-bezier $leave-delay,
padding-bottom $duration $--n-ease-in-out-cubic-bezier $leave-delay,
$original-transition;
}
&.#{$namespace}-#{$name}-transition-enter-active {
@ -160,6 +172,8 @@
opacity $duration $--n-ease-in-cubic-bezier,
margin-top $duration $--n-ease-in-out-cubic-bezier,
margin-bottom $duration $--n-ease-in-out-cubic-bezier,
padding-top $duration $--n-ease-in-out-cubic-bezier,
padding-bottom $duration $--n-ease-in-out-cubic-bezier,
$original-transition;
}
}

View File

@ -148,6 +148,9 @@ Previously, it would not work with single quotes:
35. backtop mounted blink
36. <del>Tab keep alive</del>
37. Cascader submenu 的 lightbar 用 base tracking rect 代替
38. Dropdown 样式微调
39. Card 用 padding 代替 margin 来避免 margin 折叠的问题
40. 检查 Icon 在 button 中切换有没有问题,直觉来说应该没问题
```