mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2025-01-18 12:34:25 +08:00
feat(upload): commit as a checkpoint, wip
This commit is contained in:
parent
1c497bf441
commit
2e35d3f97e
@ -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>
|
||||
```
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
130
src/Upload/src/UploadFile.vue
Normal file
130
src/Upload/src/UploadFile.vue
Normal 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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
3
think.md
3
think.md
@ -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 中切换有没有问题,直觉来说应该没问题
|
||||
|
||||
|
||||
```
|
||||
|
Loading…
Reference in New Issue
Block a user