feat(upload): wip

This commit is contained in:
07akioni 2020-02-16 21:52:38 +08:00
parent 4a40f9fb0f
commit 7b1b667792
15 changed files with 327 additions and 28 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ test-size
test-bundle
test/unit/coverage
package-lock.json
playground/temp
yarn.lock
lib
es

View File

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

View File

@ -59,6 +59,7 @@
"babel-preset-env": "^1.7.0",
"chai": "^4.2.0",
"copy-webpack-plugin": "^5.1.1",
"cors": "^2.8.5",
"cross-env": "^5.2.1",
"css-loader": "^2.1.1",
"cssnano": "^4.1.10",
@ -66,6 +67,7 @@
"emoji-unicode": "^1.1.0",
"eslint": "^6.8.0",
"eslint-plugin-vue": "^5.0.0",
"express": "^4.17.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^3.0.1",
"glob": "^7.1.6",
@ -84,9 +86,9 @@
"marked": "^0.7.0",
"memory-fs": "^0.4.1",
"mocha": "^6.2.2",
"multer": "^1.4.2",
"ncp": "^2.0.0",
"node-sass": "^4.13.1",
"sass-loader": "^7.3.1",
"postcss": "^7.0.26",
"postcss-cli": "^6.1.3",
"postcss-loader": "^3.0.0",
@ -96,6 +98,7 @@
"rollup": "^1.31.0",
"rollup-plugin-terser": "^5.2.0",
"rollup-plugin-vue": "^5.1.6",
"sass-loader": "^7.3.1",
"schema-utils": "^1.0.0",
"sinon": "^7.5.0",
"style-loader": "^0.23.1",

View File

@ -0,0 +1,15 @@
const express = require('express')
const multer = require('multer')
const cors = require('cors')
const path = require('path')
const app = express()
const upload = multer({ dest: path.resolve(__dirname, 'temp') })
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.listen(3000)

View File

@ -90,7 +90,9 @@
<div
class="n-progress-graph-line-rail"
:style="{
backgroundColor: safeRailColor
backgroundColor: safeRailColor,
height: styleHeight,
borderRadius: styleBorderRadius
}"
>
<div
@ -100,7 +102,9 @@
}"
:style="{
maxWidth: fillStyleMaxWidth + '%',
backgroundColor: safeColor
backgroundColor: safeColor,
height: styleHeight,
borderRadius: styleBorderRadius
}"
>
<div
@ -234,7 +238,7 @@ import mdCloseCircle from '../../_icons/md-close-circle'
import fontawareable from '../../_mixins/fontawarable'
import withapp from '../../_mixins/withapp'
import themeable from '../../_mixins/themeable'
import asthemecontext from '../../_mixins/asthemecontext'
import formatLength from '../../_utils/css/formatLength'
function circlePath (r, sw, vw = 100) {
return `m ${vw / 2} ${vw / 2 - r} a ${r} ${r} 0 1 1 0 ${2 * r} a ${r} ${r} 0 1 1 0 -${2 * r}`
@ -251,7 +255,7 @@ export default {
mdCheckmarkCircle,
mdCloseCircle
},
mixins: [withapp, themeable, asthemecontext, fontawareable],
mixins: [withapp, themeable, fontawareable],
props: {
processing: {
type: Boolean,
@ -316,6 +320,10 @@ export default {
circleGap: {
type: Number,
default: 1
},
height: {
type: Number,
default: null
}
},
data () {
@ -325,6 +333,13 @@ export default {
}
},
computed: {
styleHeight () {
return formatLength(this.height)
},
styleBorderRadius () {
if (this.height) return formatLength(this.height / 2)
return null
},
syntheticIndicatorPlacement () {
return this.indicatorPlacement || this.indicatorPosition
},

View File

@ -1,33 +1,125 @@
<template>
<div
class="n-upload"
@click="handleClick"
:class="{
[`n-${syntheticTheme}-theme`]: syntheticTheme
}"
>
<input
ref="input"
type="file"
class="~n-upload__file-input"
class="n-upload__file-input"
:accept="accept"
:multiple="multiple"
@change="handleFileInputChange"
>
<div>
<div v-for="(file, index) in fileList" :key="index">
{{ file.name }} <n-progress type="line" :show-indicator="false" :percentage="50" style="width: 50%;" /></n-progress>
</div>
<div class="n-upload__activator" @click="handleActivatorClick">
<slot />
</div>
<div class="n-upload-file-list">
<template v-for="(file, index) in fileList">
<div
v-if="!isFileRemoved(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>
</template>
</div>
</div>
</template>
<script>
import NProgress from '../../Progress'
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'
function XHRHandlers (componentInstance, index) {
return {
handleXHRLoad (e) {
const file = componentInstance.fileList[index]
file.status = 'done'
file.percentage = 100
console.log('!!!done')
},
handleXHRAbort (e) {
componentInstance.fileList[index].status = 'removed'
console.log('!!!abort')
},
handleXHRError (e) {
componentInstance.fileList[index].status = 'error'
console.log('!!!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
}
}
}
}
function registerHandler (request, componentInstance, index) {
const handlers = XHRHandlers(componentInstance, index)
request.onabort = handlers.handleXHRAbort
request.onerror = handlers.handleXHRError
request.onload = handlers.handleXHRLoad
if (request.upload) {
request.upload.onprogress = handlers.handleXHRProgress
}
}
function submit (method = 'POST', action, formData, index, componentInstance) {
const request = new XMLHttpRequest()
registerHandler(request, componentInstance, index)
request.open(method, action)
request.send(formData)
}
export default {
name: 'NUpload',
components: {
NProgress
NButton,
NUploadProgress,
closeOutline,
downloadOutline
},
mixins: [ withapp, themeable ],
props: {
name: {
type: String,
default: 'file'
},
accept: {
type: [String, Array],
default: null
@ -68,19 +160,45 @@ export default {
data () {
return {
fileList: [],
formData: new FormData()
formDataList: []
}
},
methods: {
handleClick () {
// this.$refs.input.click()
handleActivatorClick () {
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 currentFileList = this.fileList
this.fileList = currentFileList.concat(e.target.files)
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,
status: 'pending',
percentage: 0
})
const formData = new FormData()
formData.append(fieldName, file)
formDataList.push(formData)
this.submit(formData, memorizedFileListLength + index)
})
e.target.value = null
},
submit () {
// clear the queue of current form data
submit (formData, index) {
submit(this.method, this.action, formData, index, this)
}
}
}

View File

@ -0,0 +1,61 @@
<template>
<n-fade-in-height-expand-transition>
<n-progress
v-if="postponedShow"
type="line"
:show-indicator="false"
:percentage="percentage"
:status="status"
:height="2"
/>
</n-fade-in-height-expand-transition>
</template>
<script>
import NFadeInHeightExpandTransition from '../../_transition/FadeInHeightExpandTransition'
import NProgress from '../../Progress'
export default {
name: 'NUploadProgress',
components: {
NProgress,
NFadeInHeightExpandTransition
},
props: {
show: {
type: Boolean,
default: false
},
percentage: {
type: Number,
required: true
},
status: {
type: String,
required: true
},
delay: {
type: Number,
default: 600
}
},
data () {
return {
postponedShow: false
}
},
watch: {
show (value) {
if (value) this.postponedShow = true
else {
window.setTimeout(() => {
this.postponedShow = false
}, this.delay)
}
}
},
created () {
this.postponedShow = this.show
}
}
</script>

View File

@ -8,6 +8,10 @@ export default {
width: {
type: Boolean,
default: false
},
appear: {
type: Boolean,
default: false
}
},
beforeDestroy () {
@ -78,7 +82,8 @@ export default {
render (h) {
return h('transition', {
props: {
name: this.width ? 'n-fade-in-width-expand-transition' : 'n-fade-in-height-expand-transition'
name: this.width ? 'n-fade-in-width-expand-transition' : 'n-fade-in-height-expand-transition',
appear: this.appear
},
on: {
beforeLeave: this.handleBeforeLeave,

View File

@ -7,5 +7,70 @@
height: 0;
opacity: 0;
}
@include e(activator) {
display: inline-block;
}
@include b(upload-file-list) {
margin-top: 8px;
line-height: 1.5;
@include b(upload-file) {
cursor: pointer;
padding: 6px 12px 0 12px;
transition: background-color .3s $--n-ease-in-out-cubic-bezier;
border-radius: 6px;
&:hover {
background-color: map-get($--upload-file-item-background-color, 'hover');
@include b(upload-file-info) {
@include e(action) {
opacity: 1;
}
}
}
@include m(error-status) {
@include b(upload-file-info) {
@include e(name) {
color: $--error-6;
}
}
}
@include m(success-status) {
@include b(upload-file-info) {
@include e(name) {
color: $--success-6;
}
}
}
@include b(upload-file-info) {
position: relative;
padding-bottom: 6px;
@include e(name) {
font-size: 14px;
color: $--n-secondary-text-color;
transition: color .3s $--n-ease-in-out-cubic-bezier;
}
@include e(action) {
padding-bottom: inherit;
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 80px;
display: flex;
align-items: center;
transition: opacity .2s $--n-ease-in-out-cubic-bezier;
flex-direction: row-reverse;
@include b(button) {
&:not(:first-child) {
margin-right: 8px;
}
}
}
}
@include b(progress) {
@include fade-in-height-expand-transition;
margin-bottom: 6px;
}
}
}
}
}

View File

@ -0,0 +1,5 @@
@mixin setup-dark-upload {
$--uploadd-file-item-background-color: (
'hover': change-color($--primary-6, $alpha: .15)
) !global;
}

View File

@ -55,6 +55,7 @@
@import 'components/Result.scss';
@import 'components/Tree.scss';
@import 'components/Typography.scss';
@import 'components/Upload.scss';
@mixin setup-dark-theme($in-js-env: false) {
@include setup-dark-colors();
@ -142,5 +143,6 @@
@include setup-dark-result;
@include setup-dark-tree;
@include setup-dark-typography;
@include setup-dark-upload;
}
}

View File

@ -1,5 +1,5 @@
@mixin setup-light-progress {
$--progress-rail-color: rgba(0, 0, 0, .1) !global;
$--progress-rail-color: $--n-action-panel-color !global;
$--progress-fill-color: (
'default': $--info-6,
'info': $--info-6,

View File

@ -0,0 +1,5 @@
@mixin setup-light-upload {
$--upload-file-item-background-color: (
'hover': change-color($--primary-6, $alpha: .1)
) !global;
}

View File

@ -55,6 +55,7 @@
@import 'components/Result.scss';
@import 'components/Tree.scss';
@import 'components/Typography.scss';
@import 'components/Upload.scss';
@mixin setup-light-theme($in-js-env: false) {
@include setup-light-colors();
@ -141,5 +142,6 @@
@include setup-light-result;
@include setup-light-tree;
@include setup-light-typography;
@include setup-light-upload;
}
}

View File

@ -135,7 +135,7 @@
}
}
@mixin fade-in-height-expand-transition ($name: 'fade-in-height-expand', $duration: .3s, $delay: 0s, $original-transition: ()) {
@mixin fade-in-height-expand-transition ($name: 'fade-in-height-expand', $duration: .3s, $original-transition: (), $leave-delay: 0s) {
&.#{$namespace}-#{$name}-transition-leave, &.#{$namespace}-#{$name}-transition-enter-to {
opacity: 1;
}
@ -147,10 +147,10 @@
&.#{$namespace}-#{$name}-transition-leave-active {
overflow: hidden;
transition:
max-height $duration $--n-ease-in-out-cubic-bezier,
opacity $duration $--n-ease-out-cubic-bezier,
margin-top $duration $--n-ease-in-out-cubic-bezier,
margin-bottom $duration $--n-ease-in-out-cubic-bezier,
max-height $duration $--n-ease-in-out-cubic-bezier $leave-delay,
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,
$original-transition;
}
&.#{$namespace}-#{$name}-transition-enter-active {