Toggle navigation
Toggle navigation
This project
Loading...
Sign in
Hooke
/
custom_form
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Graphs
Network
Create a new issue
Commits
Issue Boards
Authored by
hookehuyr
2023-04-12 10:06:46 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
445cd2ba29b8ce7dc8cb1117c6c47560796b2146
445cd2ba
1 parent
19b5836b
✨ feat(上传文件控件): 样式和功能调整
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
1070 additions
and
154 deletions
components.d.ts
package.json
src/components/FileUploaderField/Uploader.js
src/components/FileUploaderField/index.vue
src/hooks/useComponentType.js
yarn.lock
components.d.ts
View file @
445cd2b
...
...
@@ -49,6 +49,8 @@ declare module '@vue/runtime-core' {
NutSwiper
:
typeof
import
(
'@nutui/nutui-taro'
)[
'Swiper'
]
NutSwiperItem
:
typeof
import
(
'@nutui/nutui-taro'
)[
'SwiperItem'
]
NutTextarea
:
typeof
import
(
'@nutui/nutui-taro'
)[
'Textarea'
]
NutToast
:
typeof
import
(
'@nutui/nutui-taro'
)[
'Toast'
]
NutUploader
:
typeof
import
(
'@nutui/nutui-taro'
)[
'Uploader'
]
PhoneField
:
typeof
import
(
'./src/components/PhoneField/index.vue'
)[
'default'
]
PickerField
:
typeof
import
(
'./src/components/PickerField/index.vue'
)[
'default'
]
RadioField
:
typeof
import
(
'./src/components/RadioField/index.vue'
)[
'default'
]
...
...
package.json
View file @
445cd2b
...
...
@@ -38,7 +38,7 @@
"dependencies"
:
{
"@babel/runtime"
:
"^7.7.7"
,
"@nutui/icons-vue-taro"
:
"^0.0.9"
,
"@nutui/nutui-taro"
:
"^4.0.
4
"
,
"@nutui/nutui-taro"
:
"^4.0.
5
"
,
"@tarojs/components"
:
"3.6.2"
,
"@tarojs/extend"
:
"^3.6.2"
,
"@tarojs/helper"
:
"3.6.2"
,
...
...
@@ -57,6 +57,7 @@
"@tarojs/taro"
:
"3.6.2"
,
"@vant/area-data"
:
"^1.4.1"
,
"axios-miniprogram"
:
"^2.0.0-rc-2"
,
"browser-md5-file"
:
"^1.1.1"
,
"dayjs"
:
"^1.11.7"
,
"pinia"
:
"2.0.10"
,
"taro-plugin-pinia"
:
"^1.0.0"
,
...
...
src/components/FileUploaderField/Uploader.js
0 → 100644
View file @
445cd2b
var
__defProp
=
Object
.
defineProperty
var
__defNormalProp
=
(
obj
,
key
,
value
)
=>
key
in
obj
?
__defProp
(
obj
,
key
,
{
enumerable
:
true
,
configurable
:
true
,
writable
:
true
,
value
,
})
:
(
obj
[
key
]
=
value
)
var
__publicField
=
(
obj
,
key
,
value
)
=>
{
__defNormalProp
(
obj
,
typeof
key
!==
'symbol'
?
key
+
''
:
key
,
value
)
return
value
}
import
{
reactive
,
computed
,
resolveComponent
,
openBlock
,
createElementBlock
,
normalizeClass
,
renderSlot
,
createTextVNode
,
createBlock
,
createCommentVNode
,
Fragment
,
renderList
,
createElementVNode
,
toDisplayString
,
createVNode
,
}
from
'vue'
import
{
c
as
createComponent
}
from
'./component-25dcca32.js'
import
{
f
as
funInterceptor
}
from
'./interceptor-157a0193.js'
import
Progress
from
'./Progress.js'
import
Button
from
'./Button.js'
import
Taro
from
'@tarojs/taro'
import
{
Photograph
,
Failure
,
Loading
,
Del
,
Link
}
from
'@nutui/icons-vue-taro'
import
{
_
as
_export_sfc
}
from
'./_plugin-vue_export-helper-cc2b3d55.js'
import
'../locale/lang'
class
UploadOptions
{
constructor
()
{
__publicField
(
this
,
'url'
,
''
)
__publicField
(
this
,
'name'
,
'file'
)
__publicField
(
this
,
'fileType'
,
'image'
)
__publicField
(
this
,
'formData'
)
__publicField
(
this
,
'sourceFile'
)
__publicField
(
this
,
'method'
,
'post'
)
__publicField
(
this
,
'xhrState'
,
200
)
__publicField
(
this
,
'timeout'
,
30
*
1
e3
)
__publicField
(
this
,
'headers'
,
{})
__publicField
(
this
,
'withCredentials'
,
false
)
__publicField
(
this
,
'onStart'
)
__publicField
(
this
,
'taroFilePath'
)
__publicField
(
this
,
'onProgress'
)
__publicField
(
this
,
'onSuccess'
)
__publicField
(
this
,
'onFailure'
)
__publicField
(
this
,
'beforeXhrUpload'
)
}
}
class
Uploader
{
constructor
(
options
)
{
__publicField
(
this
,
'options'
)
this
.
options
=
options
}
upload
()
{
var
_a
const
options
=
this
.
options
const
xhr
=
new
XMLHttpRequest
()
xhr
.
timeout
=
options
.
timeout
if
(
xhr
.
upload
)
{
xhr
.
upload
.
addEventListener
(
'progress'
,
(
e
)
=>
{
var
_a2
;(
_a2
=
options
.
onProgress
)
==
null
?
void
0
:
_a2
.
call
(
options
,
e
,
options
)
},
false
,
)
xhr
.
onreadystatechange
=
()
=>
{
var
_a2
,
_b
if
(
xhr
.
readyState
===
4
)
{
if
(
xhr
.
status
==
options
.
xhrState
)
{
;(
_a2
=
options
.
onSuccess
)
==
null
?
void
0
:
_a2
.
call
(
options
,
xhr
.
responseText
,
options
)
}
else
{
;(
_b
=
options
.
onFailure
)
==
null
?
void
0
:
_b
.
call
(
options
,
xhr
.
responseText
,
options
)
}
}
}
xhr
.
withCredentials
=
options
.
withCredentials
xhr
.
open
(
options
.
method
,
options
.
url
,
true
)
for
(
const
[
key
,
value
]
of
Object
.
entries
(
options
.
headers
))
{
xhr
.
setRequestHeader
(
key
,
value
)
}
;(
_a
=
options
.
onStart
)
==
null
?
void
0
:
_a
.
call
(
options
,
options
)
if
(
options
.
beforeXhrUpload
)
{
options
.
beforeXhrUpload
(
xhr
,
options
)
}
else
{
xhr
.
send
(
options
.
formData
)
}
}
else
{
console
.
warn
(
'浏览器不支持 XMLHttpRequest'
)
}
}
}
class
UploaderTaro
extends
Uploader
{
constructor
(
options
)
{
super
(
options
)
}
uploadTaro
(
uploadFile
,
env
)
{
var
_a
const
options
=
this
.
options
if
(
env
===
'WEB'
)
{
this
.
upload
()
}
else
{
if
(
options
.
beforeXhrUpload
)
{
options
.
beforeXhrUpload
(
uploadFile
,
options
)
}
else
{
const
uploadTask
=
uploadFile
({
url
:
options
.
url
,
filePath
:
options
.
taroFilePath
,
fileType
:
options
.
fileType
,
header
:
{
'Content-Type'
:
'multipart/form-data'
,
...
options
.
headers
,
},
//
formData
:
options
.
formData
,
name
:
options
.
name
,
success
(
response
)
{
var
_a2
,
_b
if
(
options
.
xhrState
==
response
.
statusCode
)
{
;(
_a2
=
options
.
onSuccess
)
==
null
?
void
0
:
_a2
.
call
(
options
,
response
,
options
)
}
else
{
;(
_b
=
options
.
onFailure
)
==
null
?
void
0
:
_b
.
call
(
options
,
response
,
options
)
}
},
fail
(
e
)
{
var
_a2
;(
_a2
=
options
.
onFailure
)
==
null
?
void
0
:
_a2
.
call
(
options
,
e
,
options
)
},
})
;(
_a
=
options
.
onStart
)
==
null
?
void
0
:
_a
.
call
(
options
,
options
)
uploadTask
.
progress
((
res
)
=>
{
var
_a2
;(
_a2
=
options
.
onProgress
)
==
null
?
void
0
:
_a2
.
call
(
options
,
res
,
options
)
})
}
}
}
}
const
{
translate
:
translate$1
}
=
createComponent
(
'uploader'
)
class
FileItem
{
constructor
()
{
__publicField
(
this
,
'status'
,
'ready'
)
__publicField
(
this
,
'message'
,
translate$1
(
'ready'
))
__publicField
(
this
,
'uid'
,
/* @__PURE__ */
new
Date
().
getTime
().
toString
())
__publicField
(
this
,
'name'
)
__publicField
(
this
,
'url'
)
__publicField
(
this
,
'type'
)
__publicField
(
this
,
'path'
)
__publicField
(
this
,
'percentage'
,
0
)
__publicField
(
this
,
'formData'
,
{})
}
}
const
{
componentName
,
create
,
translate
}
=
createComponent
(
'uploader'
)
const
_sfc_main
=
create
({
components
:
{
[
Progress
.
name
]:
Progress
,
[
Button
.
name
]:
Button
,
Photograph
,
Failure
,
Loading
,
Del
,
Link
,
},
props
:
{
name
:
{
type
:
String
,
default
:
'file'
},
url
:
{
type
:
String
,
default
:
''
},
sizeType
:
{
type
:
Array
,
default
:
()
=>
[
'original'
,
'compressed'
],
},
sourceType
:
{
type
:
Array
,
default
:
()
=>
[
'album'
,
'camera'
],
},
mediaType
:
{
type
:
Array
,
default
:
()
=>
[
'image'
,
'video'
,
'mix'
],
},
camera
:
{
type
:
String
,
default
:
'back'
,
},
timeout
:
{
type
:
[
Number
,
String
],
default
:
1
e3
*
30
},
// defaultFileList: { type: Array, default: () => new Array<FileItem>() },
fileList
:
{
type
:
Array
,
default
:
()
=>
[]
},
isPreview
:
{
type
:
Boolean
,
default
:
true
},
// picture、list
listType
:
{
type
:
String
,
default
:
'picture'
},
isDeletable
:
{
type
:
Boolean
,
default
:
true
},
method
:
{
type
:
String
,
default
:
'post'
},
capture
:
{
type
:
Boolean
,
default
:
false
},
maximize
:
{
type
:
[
Number
,
String
],
default
:
Number
.
MAX_VALUE
},
maximum
:
{
type
:
[
Number
,
String
],
default
:
9
},
clearInput
:
{
type
:
Boolean
,
default
:
true
},
accept
:
{
type
:
String
,
default
:
'*'
},
headers
:
{
type
:
Object
,
default
:
{}
},
data
:
{
type
:
Object
,
default
:
{}
},
xhrState
:
{
type
:
[
Number
,
String
],
default
:
200
},
multiple
:
{
type
:
Boolean
,
default
:
true
},
disabled
:
{
type
:
Boolean
,
default
:
false
},
autoUpload
:
{
type
:
Boolean
,
default
:
true
},
maxDuration
:
{
type
:
Number
,
default
:
10
},
beforeXhrUpload
:
{
type
:
Function
,
default
:
null
,
},
beforeDelete
:
{
type
:
Function
,
default
:
(
file
,
files
)
=>
{
return
true
},
},
onChange
:
{
type
:
Function
},
},
emits
:
[
'start'
,
'progress'
,
'oversize'
,
'success'
,
'failure'
,
'change'
,
'delete'
,
'update:fileList'
,
'file-item-click'
,
],
setup
(
props
,
{
emit
})
{
const
fileList
=
reactive
(
props
.
fileList
)
let
uploadQueue
=
[]
const
classes
=
computed
(()
=>
{
const
prefixCls
=
componentName
return
{
[
prefixCls
]:
true
,
}
})
const
chooseImage
=
()
=>
{
if
(
props
.
disabled
)
{
return
}
if
(
Taro
.
getEnv
()
==
'WEB'
)
{
let
el
=
document
.
getElementById
(
'taroChooseImage'
)
if
(
el
)
{
el
==
null
?
void
0
:
el
.
setAttribute
(
'accept'
,
props
.
accept
)
}
else
{
const
obj
=
document
.
createElement
(
'input'
)
obj
.
setAttribute
(
'type'
,
'file'
)
obj
.
setAttribute
(
'id'
,
'taroChooseImage'
)
obj
.
setAttribute
(
'accept'
,
props
.
accept
)
obj
.
setAttribute
(
'style'
,
'position: fixed; top: -4000px; left: -3000px; z-index: -300;'
,
)
document
.
body
.
appendChild
(
obj
)
}
}
if
(
Taro
.
getEnv
()
==
'WEAPP'
)
{
// 上传文件类型
if
(
props
.
mediaType
.
includes
(
'file'
))
{
wx
.
chooseMessageFile
({
/** 最多可以选择的文件个数 */
count
:
props
.
multiple
?
props
.
maximum
*
1
-
props
.
fileList
.
length
:
1
,
type
:
'file'
,
/** 接口调用失败的回调函数 */
fail
:
(
res
)
=>
{
emit
(
'failure'
,
res
)
},
/** 接口调用成功的回调函数 */
success
:
onChangeFile
,
})
}
else
{
Taro
.
chooseMedia
({
/** 最多可以选择的文件个数 */
count
:
props
.
multiple
?
props
.
maximum
*
1
-
props
.
fileList
.
length
:
1
,
/** 文件类型 */
mediaType
:
props
.
mediaType
,
/** 图片和视频选择的来源 */
sourceType
:
props
.
sourceType
,
/** 拍摄视频最长拍摄时间,单位秒。时间范围为 3s 至 30s 之间 */
maxDuration
:
props
.
maxDuration
,
/** 仅对 mediaType 为 image 时有效,是否压缩所选文件 */
sizeType
:
[],
/** 仅在 sourceType 为 camera 时生效,使用前置或后置摄像头 */
camera
:
props
.
camera
,
/** 接口调用失败的回调函数 */
fail
:
(
res
)
=>
{
emit
(
'failure'
,
res
)
},
/** 接口调用成功的回调函数 */
success
:
onChangeMedia
,
})
}
}
else
{
Taro
.
chooseImage
({
// 选择数量
count
:
props
.
multiple
?
props
.
maximum
*
1
-
props
.
fileList
.
length
:
1
,
// 可以指定是原图还是压缩图,默认二者都有
sizeType
:
props
.
sizeType
,
sourceType
:
props
.
sourceType
,
success
:
onChangeImage
,
fail
:
(
res
)
=>
{
emit
(
'failure'
,
res
)
},
})
}
}
const
onChangeMedia
=
(
res
)
=>
{
const
{
type
,
tempFiles
}
=
res
const
_files
=
filterFiles
(
tempFiles
)
readFile
(
_files
)
emit
(
'change'
,
{
fileList
,
})
}
const
onChangeFile
=
(
res
)
=>
{
const
{
type
,
tempFiles
}
=
res
const
_files
=
filterFiles
(
tempFiles
)
readFile
(
_files
)
emit
(
'change'
,
{
fileList
,
})
}
const
onChangeImage
=
(
res
)
=>
{
const
{
tempFilePaths
,
tempFiles
}
=
res
const
_files
=
filterFiles
(
tempFiles
)
readFile
(
_files
)
emit
(
'change'
,
{
fileList
,
})
}
const
fileItemClick
=
(
fileItem
)
=>
{
emit
(
'file-item-click'
,
{
fileItem
})
}
const
executeUpload
=
(
fileItem
,
index
)
=>
{
const
uploadOption
=
new
UploadOptions
()
uploadOption
.
name
=
props
.
name
uploadOption
.
url
=
props
.
url
uploadOption
.
fileType
=
fileItem
.
type
uploadOption
.
formData
=
fileItem
.
formData
uploadOption
.
timeout
=
props
.
timeout
*
1
uploadOption
.
method
=
props
.
method
uploadOption
.
xhrState
=
props
.
xhrState
uploadOption
.
method
=
props
.
method
uploadOption
.
headers
=
props
.
headers
uploadOption
.
taroFilePath
=
fileItem
.
path
uploadOption
.
beforeXhrUpload
=
props
.
beforeXhrUpload
uploadOption
.
onStart
=
(
option
)
=>
{
fileItem
.
status
=
'ready'
fileItem
.
message
=
translate
(
'readyUpload'
)
clearUploadQueue
(
index
)
emit
(
'start'
,
option
)
}
uploadOption
.
onProgress
=
(
event
,
option
)
=>
{
fileItem
.
status
=
'uploading'
fileItem
.
message
=
translate
(
'uploading'
)
fileItem
.
percentage
=
event
.
progress
emit
(
'progress'
,
{
event
,
option
,
percentage
:
fileItem
.
percentage
})
}
uploadOption
.
onSuccess
=
(
data
,
option
)
=>
{
fileItem
.
status
=
'success'
fileItem
.
message
=
translate
(
'success'
)
emit
(
'success'
,
{
data
,
responseText
:
data
,
option
,
fileItem
,
})
emit
(
'update:fileList'
,
fileList
)
}
uploadOption
.
onFailure
=
(
data
,
option
)
=>
{
fileItem
.
status
=
'error'
fileItem
.
message
=
translate
(
'error'
)
emit
(
'failure'
,
{
data
,
responseText
:
data
,
option
,
fileItem
,
})
}
let
task
=
new
UploaderTaro
(
uploadOption
)
if
(
props
.
autoUpload
)
{
task
.
uploadTaro
(
Taro
.
uploadFile
,
Taro
.
getEnv
())
}
else
{
uploadQueue
.
push
(
new
Promise
((
resolve
,
reject
)
=>
{
resolve
(
task
)
}),
)
}
}
const
clearUploadQueue
=
(
index
=
-
1
)
=>
{
if
(
index
>
-
1
)
{
uploadQueue
.
splice
(
index
,
1
)
}
else
{
uploadQueue
=
[]
fileList
.
splice
(
0
,
fileList
.
length
)
}
}
const
submit
=
()
=>
{
Promise
.
all
(
uploadQueue
).
then
((
res
)
=>
{
res
.
forEach
((
i
)
=>
i
.
uploadTaro
(
Taro
.
uploadFile
,
Taro
.
getEnv
()))
})
}
const
readFile
=
(
files
)
=>
{
files
.
forEach
((
file
,
index
)
=>
{
var
_a
,
_b
let
fileType
=
file
.
type
let
filepath
=
file
.
tempFilePath
||
file
.
path
const
fileItem
=
reactive
(
new
FileItem
())
if
(
file
.
fileType
)
{
fileType
=
file
.
fileType
}
else
{
const
imgReg
=
/
\.(
png|jpeg|jpg|webp|gif
)
$/i
if
(
!
fileType
&&
(
imgReg
.
test
(
filepath
)
||
filepath
.
includes
(
'data:image'
))
)
{
fileType
=
'image'
}
}
fileItem
.
path
=
filepath
fileItem
.
name
=
file
.
name
?
file
.
name
:
filepath
fileItem
.
status
=
'ready'
fileItem
.
message
=
translate
(
'waitingUpload'
)
fileItem
.
type
=
fileType
if
(
Taro
.
getEnv
()
==
'WEB'
)
{
const
formData
=
new
FormData
()
for
(
const
[
key
,
value
]
of
Object
.
entries
(
props
.
data
))
{
formData
.
append
(
key
,
value
)
}
formData
.
append
(
props
.
name
,
file
.
originalFileObj
)
fileItem
.
name
=
(
_a
=
file
.
originalFileObj
)
==
null
?
void
0
:
_a
.
name
fileItem
.
type
=
(
_b
=
file
.
originalFileObj
)
==
null
?
void
0
:
_b
.
type
fileItem
.
formData
=
formData
}
else
{
fileItem
.
formData
=
props
.
data
}
if
(
props
.
isPreview
)
{
fileItem
.
url
=
fileType
==
'video'
?
file
.
thumbTempFilePath
:
filepath
}
fileList
.
push
(
fileItem
)
executeUpload
(
fileItem
,
index
)
})
}
const
filterFiles
=
(
files
)
=>
{
const
maximum
=
props
.
maximum
*
1
const
maximize
=
props
.
maximize
*
1
const
oversizes
=
new
Array
()
files
=
files
.
filter
((
file
)
=>
{
if
(
file
.
size
>
maximize
)
{
oversizes
.
push
(
file
)
return
false
}
else
{
return
true
}
})
if
(
oversizes
.
length
)
{
emit
(
'oversize'
,
oversizes
)
}
let
currentFileLength
=
files
.
length
+
fileList
.
length
if
(
currentFileLength
>
maximum
)
{
files
.
splice
(
files
.
length
-
(
currentFileLength
-
maximum
))
}
return
files
}
const
deleted
=
(
file
,
index
)
=>
{
fileList
.
splice
(
index
,
1
)
emit
(
'delete'
,
{
file
,
fileList
,
index
,
})
}
const
onDelete
=
(
file
,
index
)
=>
{
clearUploadQueue
(
index
)
funInterceptor
(
props
.
beforeDelete
,
{
args
:
[
file
,
fileList
],
done
:
()
=>
deleted
(
file
,
index
),
})
}
return
{
onDelete
,
fileList
,
classes
,
chooseImage
,
fileItemClick
,
clearUploadQueue
,
submit
,
}
},
})
const
_hoisted_1
=
{
key
:
0
,
class
:
'nut-uploader__slot'
,
}
const
_hoisted_2
=
{
key
:
0
,
class
:
'nut-uploader__preview-img'
,
}
const
_hoisted_3
=
{
key
:
0
,
class
:
'nut-uploader__preview__progress'
,
}
const
_hoisted_4
=
{
class
:
'nut-uploader__preview__progress__msg'
}
const
_hoisted_5
=
[
'onClick'
]
const
_hoisted_6
=
[
'onClick'
,
'src'
]
const
_hoisted_7
=
{
key
:
3
,
class
:
'nut-uploader__preview-img__file'
,
}
const
_hoisted_8
=
[
'onClick'
]
const
_hoisted_9
=
{
class
:
'file__name_tips'
}
const
_hoisted_10
=
{
class
:
'tips'
}
const
_hoisted_11
=
{
key
:
1
,
class
:
'nut-uploader__preview-list'
,
}
const
_hoisted_12
=
[
'onClick'
]
const
_hoisted_13
=
{
class
:
'file__name_tips'
}
function
_sfc_render
(
_ctx
,
_cache
,
$props
,
$setup
,
$data
,
$options
)
{
const
_component_nut_button
=
resolveComponent
(
'nut-button'
)
const
_component_Failure
=
resolveComponent
(
'Failure'
)
const
_component_Loading
=
resolveComponent
(
'Loading'
)
const
_component_Link
=
resolveComponent
(
'Link'
)
const
_component_Del
=
resolveComponent
(
'Del'
)
const
_component_nut_progress
=
resolveComponent
(
'nut-progress'
)
const
_component_Photograph
=
resolveComponent
(
'Photograph'
)
return
(
openBlock
(),
createElementBlock
(
'view'
,
{
class
:
normalizeClass
(
_ctx
.
classes
),
},
[
_ctx
.
$slots
.
default
?
(
openBlock
(),
createElementBlock
(
'view'
,
_hoisted_1
,
[
renderSlot
(
_ctx
.
$slots
,
'default'
),
createTextVNode
(),
Number
(
_ctx
.
maximum
)
-
_ctx
.
fileList
.
length
?
(
openBlock
(),
createBlock
(
_component_nut_button
,
{
key
:
0
,
class
:
'nut-uploader__input'
,
onClick
:
_ctx
.
chooseImage
,
},
null
,
8
,
[
'onClick'
],
))
:
createCommentVNode
(
''
,
true
),
]))
:
createCommentVNode
(
''
,
true
),
createTextVNode
(),
(
openBlock
(
true
),
createElementBlock
(
Fragment
,
null
,
renderList
(
_ctx
.
fileList
,
(
item
,
index
)
=>
{
return
(
openBlock
(),
createElementBlock
(
'view'
,
{
class
:
normalizeClass
([
'nut-uploader__preview'
,
[
_ctx
.
listType
],
]),
key
:
item
.
uid
,
},
[
_ctx
.
listType
==
'picture'
&&
!
_ctx
.
$slots
.
default
?
(
openBlock
(),
createElementBlock
(
'view'
,
_hoisted_2
,
[
item
.
status
!=
'success'
?
(
openBlock
(),
createElementBlock
(
'view'
,
_hoisted_3
,
[
item
.
status
!=
'ready'
?
(
openBlock
(),
createElementBlock
(
Fragment
,
{
key
:
0
},
[
item
.
status
==
'error'
?
(
openBlock
(),
createBlock
(
_component_Failure
,
{
key
:
0
,
color
:
'#fff'
,
}))
:
(
openBlock
(),
createBlock
(
_component_Loading
,
{
key
:
1
,
name
:
'loading'
,
color
:
'#fff'
,
})),
],
64
,
))
:
createCommentVNode
(
''
,
true
),
createTextVNode
(),
createElementVNode
(
'view'
,
_hoisted_4
,
toDisplayString
(
item
.
message
),
1
,
),
]))
:
createCommentVNode
(
''
,
true
),
createTextVNode
(),
_ctx
.
isDeletable
?
(
openBlock
(),
createElementBlock
(
'view'
,
{
key
:
1
,
class
:
'close'
,
onClick
:
(
$event
)
=>
_ctx
.
onDelete
(
item
,
index
),
},
[
renderSlot
(
_ctx
.
$slots
,
'delete-icon'
,
{},
()
=>
[
createVNode
(
_component_Failure
)],
),
],
8
,
_hoisted_5
,
))
:
createCommentVNode
(
''
,
true
),
createTextVNode
(),
[
'image'
,
'video'
].
includes
(
item
.
type
)
&&
item
.
url
?
(
openBlock
(),
createElementBlock
(
'img'
,
{
key
:
2
,
class
:
'nut-uploader__preview-img__c'
,
mode
:
'aspectFit'
,
onClick
:
(
$event
)
=>
_ctx
.
fileItemClick
(
item
),
src
:
item
.
url
,
},
null
,
8
,
_hoisted_6
,
))
:
(
openBlock
(),
createElementBlock
(
'view'
,
_hoisted_7
,
[
createElementVNode
(
'view'
,
{
class
:
'nut-uploader__preview-img__file__name'
,
onClick
:
(
$event
)
=>
_ctx
.
fileItemClick
(
item
),
},
[
createElementVNode
(
'view'
,
_hoisted_9
,
toDisplayString
(
item
.
name
),
1
,
),
],
8
,
_hoisted_8
,
),
])),
createTextVNode
(),
createElementVNode
(
'view'
,
_hoisted_10
,
toDisplayString
(
item
.
name
),
1
,
),
]))
:
_ctx
.
listType
==
'list'
?
(
openBlock
(),
createElementBlock
(
'view'
,
_hoisted_11
,
[
createElementVNode
(
'view'
,
{
class
:
normalizeClass
([
'nut-uploader__preview-img__file__name'
,
[
item
.
status
],
]),
onClick
:
(
$event
)
=>
_ctx
.
fileItemClick
(
item
),
},
[
createVNode
(
_component_Link
,
{
class
:
'nut-uploader__preview-img__file__link'
,
}),
createTextVNode
(),
createElementVNode
(
'view'
,
_hoisted_13
,
toDisplayString
(
item
.
name
),
1
,
),
createTextVNode
(),
_ctx
.
isDeletable
?
(
openBlock
(),
createBlock
(
_component_Del
,
{
key
:
0
,
color
:
'#808080'
,
class
:
'nut-uploader__preview-img__file__del'
,
onClick
:
(
$event
)
=>
_ctx
.
onDelete
(
item
,
index
),
},
null
,
8
,
[
'onClick'
],
))
:
createCommentVNode
(
''
,
true
),
],
10
,
_hoisted_12
,
),
createTextVNode
(),
item
.
status
==
'uploading'
?
(
openBlock
(),
createBlock
(
_component_nut_progress
,
{
key
:
0
,
size
:
'small'
,
percentage
:
item
.
percentage
,
'stroke-color'
:
'linear-gradient(270deg, rgba(18,126,255,1) 0%,rgba(32,147,255,1) 32.815625%,rgba(13,242,204,1) 100%)'
,
'show-text'
:
false
,
},
null
,
8
,
[
'percentage'
],
))
:
createCommentVNode
(
''
,
true
),
]))
:
createCommentVNode
(
''
,
true
),
],
2
,
)
)
}),
128
,
)),
createTextVNode
(),
_ctx
.
listType
==
'picture'
&&
!
_ctx
.
$slots
.
default
&&
Number
(
_ctx
.
maximum
)
-
_ctx
.
fileList
.
length
?
(
openBlock
(),
createElementBlock
(
'view'
,
{
key
:
1
,
class
:
normalizeClass
([
'nut-uploader__upload'
,
[
_ctx
.
listType
],
]),
},
[
renderSlot
(
_ctx
.
$slots
,
'upload-icon'
,
{},
()
=>
[
createVNode
(
_component_Photograph
,
{
color
:
'#808080'
}),
]),
createTextVNode
(),
createVNode
(
_component_nut_button
,
{
class
:
normalizeClass
([
'nut-uploader__input'
,
{
disabled
:
_ctx
.
disabled
},
]),
onClick
:
_ctx
.
chooseImage
,
},
null
,
8
,
[
'class'
,
'onClick'
],
),
],
2
,
))
:
createCommentVNode
(
''
,
true
),
],
2
,
)
)
}
const
index_taro
=
/* @__PURE__ */
_export_sfc
(
_sfc_main
,
[
[
'render'
,
_sfc_render
],
])
export
{
index_taro
as
default
}
src/components/FileUploaderField/index.vue
View file @
445cd2b
<!--
* @Date: 2022-08-31 16:16:49
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-0
2-10 11:17:21
* @FilePath: /
data-table
/src/components/FileUploaderField/index.vue
* @LastEditTime: 2023-0
4-12 09:54:33
* @FilePath: /
custom_form
/src/components/FileUploaderField/index.vue
* @Description: 文件上传控件
-->
<template>
<div v-if="HideShow" class="file-uploader-field">
<div class="label">
<
span v-if="item.component_props.required"> *</span
>
<
text v-if="item.component_props.required"> *</text
>
{{ item.component_props.label }}
</div>
<div style="font-size: 12px; color: red; margin-left: 20px;">
<text>最大文件个数为 {{ item.component_props.max_count }} 个</text>,
<text>单个文件最大体积 {{ item.component_props.max_size }} MB</text>
</div>
<div
v-if="item.component_props.note"
v-html="item.component_props.note"
style="font-size: 0.9rem; margin-left: 1rem; color: gray; padding-bottom: 0.5rem; padding-top: 0.25rem; white-space: pre-wrap;"
/>
<div>
<
!-- <
div>
<p
v-for="(file, index) in fileList"
:key="index"
style="padding-left: 1rem; margin-bottom: 0.5rem"
>
<p style="font-size: 1rem; word-break: break-all; margin-right: 0.75rem;">
<span>{{ index + 1 }}. {{ file.
file
name }} {{ (file.size / 1024 / 1024).toFixed(2) }}MB</span>
<span>{{ index + 1 }}. {{ file.name }} {{ (file.size / 1024 / 1024).toFixed(2) }}MB</span>
<span style="color: #e32525; font-size: 0.85rem" @click="beforeDelete(file)">移除</span>
</p>
</p>
</div>
<div style="padding: 1rem">
<
van
-uploader
</div>
-->
<div style="padding: 1rem
; padding-top: 0.5rem;
">
<
nut
-uploader
:name="item.name"
upload-icon="add"
accept="*"
:before-read="beforeRead"
:after-read="afterRead"
:before-delete="beforeDelete"
:multiple="item.component_props.max_size > 1"
v-model:file-list="defaultFileList"
:maximum="item.component_props.max_count"
:multiple="item.component_props.max_count > 1"
:size-type="['compressed']"
:media-type="['file']"
:maximize="max_size"
list-type="list"
:before-xhr-upload="beforeXhrUpload"
@oversize="onOversize"
@success="uploadSuccess"
@failure="uploadFailure"
@delete="onDelete"
@change="onChange"
@file-item-click="fileItemClick"
>
<van-button icon="plus" type="primary">上传文件</van-button>
</van-uploader>
<nut-button shape="square" type="primary">
<template #icon>
<Uploader />
</template>
上传文件
</nut-button>
</nut-uploader>
</div>
<!-- <div class="type-text">上传格式:{{ type_text }}</div> -->
<div
v-if="show_empty"
class="van-field__error-message"
style="padding: 0 1rem 1rem 1rem"
v-if="show_error"
style="padding: 5px 20px; color: red; font-size: 12px;"
>
文件上传不能为空
{{ error_msg }}
</div>
<van-divider />
<nut-divider :style="{ color: '#ebedf0' }" />
<nut-overlay v-model:visible="loading">
<div class="wrapper" style="color: white; font-size: 15px;">
<Loading />
上传中...
</div>
<van-overlay :show="loading">
<div class="wrapper" @click.stop>
<van-loading vertical color="#FFFFFF">上传中...</van-loading>
</nut-overlay>
<nut-toast :msg="toast_msg" v-model:visible="toast_show" :type="toast_type" />
</div>
</van-overlay>
</template>
<script setup>
/**
* 文件上传
* @param name[String] 组件名称
* @param file_type[Array] 文件上传类型
* @param multiple[Boolean] 文件多选
*/
import { showSuccessToast, showFailToast, showToast } from "vant";
import _ from "lodash";
import { v4 as uuidv4 } from "uuid";
import { ref, computed, watch, onMounted, reactive } from "vue";
import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from "@/api/common";
import BMF from "browser-md5-file";
import {
useRoute } from "vue-router
";
import
axios from "axios"
;
import
{ getEtag } from "@/utils/qetag.js"; // 生成hash值
import {
getUrlParams } from "@/utils/tools
";
import
{ Uploader, Loading } from '@nutui/icons-vue-taro'
;
import
Taro from '@tarojs/taro'
const $route = useRoute();
const props = defineProps({
item: Object,
});
...
...
@@ -84,20 +93,166 @@ const props = defineProps({
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const emit = defineEmits(["active"]);
const show_empty = ref(false);
// 文件类型中文页面显示
const type_text = computed(() => {
return props.item.component_props.file_type;
});
// // 文件类型中文页面显示
// const type_text = computed(() => {
// return props.item.component_props.file_type;
// });
// 上传文件集合
const fileList = ref([
// { url: "https://fastly.jsdelivr.net/npm/@vant/assets/leaf.jpeg" },
// Uploader 根据文件后缀来判断是否为文件文件
// 如果文件 URL 中不包含类型信息,可以添加 isImage 标记来声明
// { url: 'https://cloud-image', isImage: true },
]);
const fileList = ref([]);
const defaultFileList = ref([])
// 上传文件体积
const max_size = computed(() => {
return props.item.component_props.max_size * 1024 * 1024
})
const toast_msg = ref('');
const toast_show = ref(false);
const toast_type = ref('success');
// 超过体积大小回调
const onOversize = (files) => {
toast_msg.value = `最大文件体积为${props.item.component_props.max_size}MB`
toast_show.value = true;
toast_type.value = 'warn';
};
// 自定义上传逻辑
const beforeXhrUpload = async (xhr, options) => {
// H5环境
if (process.env.TARO_ENV === 'h5') {
// 把本地路径转换成file实体
const imgObj = defaultFileList.value[defaultFileList.value.length - 1];
const imgBlob = await fetch(imgObj.url).then(r => r.blob());
const imgFile = new File([imgBlob], imgObj.name , { type: imgObj.type });
// 上传返回file数据结构
const resImgObj = await handleUpload(imgFile);
// 上传失败提示
if (!resImgObj.src) {
options.onFailure?.(resImgObj, options);
loading.value = false;
} else {
defaultFileList.value[defaultFileList.value.length - 1]['url'] = resImgObj.src;
fileList.value.push({
name: imgFile.name,
url: resImgObj.src,
size: imgFile.size
});
options.onSuccess?.(resImgObj, options);
loading.value = false;
}
} else {
const imgObj = defaultFileList.value[defaultFileList.value.length - 1];
const fs = Taro.getFileSystemManager()
fs.getFileInfo({
filePath: imgObj.url,
success: async (res) => {
const file_info = res;
let suffix = /\.[^\.]+$/.exec(imgObj.name); // 获取后缀
// 获取七牛token
const filename = imgObj.name; // 真实文件名
const getToken = await qiniuTokenAPI({
name: filename,
hash: file_info.digest,
});
// 文件上传七牛云
// 第一次上传
if (getToken.token) {
// 自拍图片上传七牛服务器
Taro.uploadFile({
url: 'https://up.qbox.me',
filePath: imgObj.url,
name: `file`,
formData: {
token: getToken.token,
key: `uploadForm/${formCode}/${file_info.digest}${suffix}`
},
})
.then(async (res) => {
res.data = JSON.parse(res.data);
if (res.data.filekey) {
// 保存文件
const { data } = await saveFileAPI({
name: filename,
filekey: res.data.filekey,
hash: file_info.digest,
// format: image_info.format,
// height: image_info.height,
// width: image_info.width,
});
// 加入上传成功队列
fileList.value.push({
name: filename,
url: data.src,
size: file_info.size
});
options.onSuccess?.(data, options);
}
})
.catch((error) => {
console.error(error)
})
}
// 重复上传
if (getToken.data) {
// 加入上传成功队列
fileList.value.push({
name: filename,
url: getToken.data.src,
size: file_info.size
});
options.onSuccess?.(getToken.data, options);
}
}
})
}
}
// 上传成功回调
const uploadSuccess = async ({ data, fileItem, option, responseText }) => {
props.item.value = {
key: "file_uploader",
filed_name: props.item.key,
value: fileList.value,
};
// 完整数据回调到表单上
emit("active", props.item.value);
// 校验数据
validFileUploader();
};
// 上传失败回调
const uploadFailure = async ({ data, fileItem, option, responseText }) => {
console.error("上传失败", "fail");
toast_msg.value = '上传失败,请重新尝试!'
toast_show.value = true;
toast_type.value = 'fail';
};
// 删除上传队列回调
const onDelete = ({ file }) => {
fileList.value = fileList.value.filter((item) => {
if (item.url !== file.url) return item;
});
props.item.value = {
key: "file_uploader",
filed_name: props.item.key,
value: fileList.value,
};
// 完整数据回调到表单上
emit("active", props.item.value);
}
// 上传成功,点击队列项回调
const fileItemClick = (fileItem) => {
console.warn(fileItem);
}
const onChange = ({ fileList }) => {
}
// 上传前置处理
const beforeRead = (file) => {
...
...
@@ -141,82 +296,25 @@ const beforeRead = (file) => {
return flag;
};
// 文件读取完成后的回调函数
const afterRead = async (files) => {
if (Array.isArray(files)) {
// 多张文件上传files是一个数组
muliUpload(files);
} else {
const imgUrl = await handleUpload(files);
// 上传失败提示
if (!imgUrl.src) {
files.status = "failed";
files.message = "上传失败";
loading.value = false;
} else {
files.status = "";
files.message = "";
fileList.value.push({
// meta_id: imgUrl.meta_id,
name: files.file.name,
url: imgUrl.src,
size: files.file.size
// isImage: true,
});
loading.value = false;
}
}
// 过滤非包含URL的文件
fileList.value = fileList.value.filter((item) => {
if (item.url) return item;
});
props.item.value = {
key: "file_uploader",
filed_name: props.item.key,
// value: fileList.value.map((item) => item.url),
value: fileList.value,
};
show_empty.value = false;
// 完整数据回调到表单上
emit("active", props.item.value);
};
// 文件删除前的回调函数
const beforeDelete = (files) => {
fileList.value = fileList.value.filter((item) => {
if (item.url !== files.url) return item;
});
props.item.value = {
key: "file_uploader",
filed_name: props.item.key,
// value: fileList.value.map((item) => item.url),
value: fileList.value,
};
// 完整数据回调到表单上
emit("active", props.item.value);
};
/********** 上传七牛云获取文件地址 ***********/
const loading = ref(false);
const formCode =
$route.query.code
; // 表单code
const formCode =
getUrlParams(location.href) ? getUrlParams(location.href).code : ''
; // 表单code
// 上传文件返回文件URL
const handleUpload = async (file
s
) => {
const handleUpload = async (file) => {
loading.value = true;
// 获取HASH值
// const hash = getEtag(files.content);
return new Promise((resolve, reject) => {
// 获取MD5值
const bmf = new BMF();
bmf.md5(
file
s.file
,
file,
async (err, md5) => {
if (err) {
console.log(err);
reject(err);
}
// 获取七牛token
const filename = file
s.file
.name; // 真实文件名
const filename = file.name; // 真实文件名
const getToken = await qiniuTokenAPI({
name: filename,
hash: md5,
...
...
@@ -225,10 +323,8 @@ const handleUpload = async (files) => {
let imgUrl = "";
// 第一次上传
if (getToken.token) {
files.status = "uploading";
files.message = "上传中...";
// 返回数据库真实文件地址
imgUrl = await uploadQiniu(file
s.file
, getToken.token, filename, md5);
imgUrl = await uploadQiniu(file, getToken.token, filename, md5);
}
// 重复上传
if (getToken.data) {
...
...
@@ -243,30 +339,6 @@ const handleUpload = async (files) => {
});
};
// 多选文件上传遍历
var muliUpload = async (files) => {
for (let item of files) {
const res = await handleUpload(item);
// 上传失败提示
if (!res.src) {
item.status = "failed";
item.message = "上传失败";
loading.value = false;
} else {
item.status = "";
item.message = "";
fileList.value.push({
// meta_id: res.meta_id,
name: item.file.name,
url: res.src,
size: files.file.size
// isImage: true,
});
loading.value = false;
}
}
};
// 生成数据库真实文件地址
const uploadQiniu = async (file, token, name, md5) => {
let suffix = /\.[^\.]+$/.exec(name); // 获取后缀
...
...
@@ -307,28 +379,34 @@ const uploadQiniu = async (file, token, name, md5) => {
/****************** END *******************/
const show_error = ref(false);
const error_msg = ref('');
// 校验模块
const validFileUploader = () => {
// 必填项 未上传文件
if (props.item.component_props.required && !fileList.value.length) {
show_empty.value = true;
show_error.value = true;
error_msg.value = '必填项不能为空'
} else {
show_empty.value = false;
show_error.value = false;
error_msg.value = ''
}
return !show_e
mpty
.value;
return !show_e
rror
.value;
};
defineExpose({ validFileUploader });
</script>
<style lang="less"
scoped
>
<style lang="less">
.file-uploader-field {
.label {
padding: 1rem 1rem 0 1rem;
font-size: 0.9rem;
margin-left: 1rem;
padding-bottom: 20px;
font-size: 26px;
font-weight: bold;
span
{
text
{
color: red;
}
}
...
...
src/hooks/useComponentType.js
View file @
445cd2b
...
...
@@ -9,7 +9,7 @@ import DatePickerField from '@/components/DatePickerField/index.vue'
import
TimePickerField
from
'@/components/TimePickerField/index.vue'
import
DateTimePickerField
from
'@/components/DateTimePickerField/index.vue'
// import ImageUploaderField from '@/components/ImageUploaderField/index.vue'
//
import FileUploaderField from '@/components/FileUploaderField/index.vue'
import
FileUploaderField
from
'@/components/FileUploaderField/index.vue'
import
PhoneField
from
'@/components/PhoneField/index.vue'
import
EmailField
from
'@/components/EmailField/index.vue'
// import SignField from '@/components/SignField/index.vue'
...
...
@@ -108,9 +108,9 @@ export function createComponentType(data) {
// if (item.component_props.tag === 'image_uploader') {
// item.component = ImageUploaderField
// }
//
if (item.component_props.tag === 'file_uploader') {
//
item.component = FileUploaderField
//
}
if
(
item
.
component_props
.
tag
===
'file_uploader'
)
{
item
.
component
=
FileUploaderField
}
if
(
item
.
component_props
.
tag
===
'phone'
)
{
item
.
name
=
item
.
key
item
.
component
=
PhoneField
...
...
yarn.lock
View file @
445cd2b
...
...
@@ -1359,10 +1359,10 @@
resolved "https://mirrors.cloud.tencent.com/npm/@nutui/icons-vue-taro/-/icons-vue-taro-0.0.9.tgz#b5223eb01e2b987fdbe460e5d0439a66481e54f1"
integrity sha512-10VYAtFC+o1X0anGs+y2PgF1NWMeLFz2JVMRw4BWLg6wbtVbYy9wukLxyGhZC6Yf6t39DcwaGVda8paV7K6/Ew==
"@nutui/nutui-taro@^4.0.
4
":
version "4.0.
4
"
resolved "https://mirrors.cloud.tencent.com/npm/@nutui/nutui-taro/-/nutui-taro-4.0.
4.tgz#c5b65431ece527e3e531bc7923ad0a7b499daeeb
"
integrity sha512-
v9XyXidgiRgZTH5JofgjhJTGGvQDXhOqkLb/uRjlR2c9eQBSU653rlTO82VjUWOpCterYDxtqXswUZIf+PRbfQ
==
"@nutui/nutui-taro@^4.0.
5
":
version "4.0.
5
"
resolved "https://mirrors.cloud.tencent.com/npm/@nutui/nutui-taro/-/nutui-taro-4.0.
5.tgz#67c88d582e921641e81432ae1fd136e10e5bc1ea
"
integrity sha512-
j7+fKtRarfphYJ0uosdalIWvf3k2+x8eDsmBSSnPDyGEeRHtXqGIw8MIxy8wL5UUnADP+6JVK339vC28oo436g
==
dependencies:
"@nutui/icons-vue-taro" "^0.0.9"
sass "^1.50.0"
...
...
@@ -3546,6 +3546,13 @@ braces@^3.0.2, braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
browser-md5-file@^1.1.1:
version "1.1.1"
resolved "https://mirrors.cloud.tencent.com/npm/browser-md5-file/-/browser-md5-file-1.1.1.tgz#247d63527f662d9667adecbe61808b4961b90dc6"
integrity sha512-9h2UViTtZPhBa7oHvp5mb7MvJaX5OKEPUsplDwJ800OIV+In7BOR3RXOMB78obn2iQVIiS3WkVLhG7Zu1EMwbw==
dependencies:
spark-md5 "^2.0.2"
browser-process-hrtime@^1.0.0:
version "1.0.0"
resolved "https://mirrors.cloud.tencent.com/npm/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
...
...
@@ -11519,6 +11526,11 @@ sourcemap-codec@^1.4.8:
resolved "https://mirrors.cloud.tencent.com/npm/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
spark-md5@^2.0.2:
version "2.0.2"
resolved "https://mirrors.cloud.tencent.com/npm/spark-md5/-/spark-md5-2.0.2.tgz#37b763847763ae7e7acef2ca5233d01e649a78b7"
integrity sha1-N7djhHdjrn56zvLKUjPQHmSaeLc=
spdx-correct@^3.0.0:
version "3.2.0"
resolved "https://mirrors.cloud.tencent.com/npm/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c"
...
...
Please
register
or
login
to post a comment