合同小程序前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

262 lines
8.1 KiB

1 week ago
  1. <template>
  2. <view class="l-upload" ref="uploadRef" :style="[styles]">
  3. <view class="l-upload__item" v-for="(file, index) in customFiles" :style="[itemStyle]" :key="file.url">
  4. <view class="l-upload__item-inner" :style="[innerStyle]">
  5. <slot name="file" :file="file" :index="index">
  6. <image class="l-upload__image" v-if="file.type == 'image'" :src="file.url" :data-file="file"
  7. :mode="imageFit" @click="onProofTap(index)" />
  8. <video class="l-upload__image" v-if="file.type == 'video'" :src="file.url" :data-file="file"
  9. :poster="file.thumb" :autoplay="false" objectFit="contain" @click="onFileClick(index)" />
  10. <view class="l-upload__progress-mask" v-if="file.status != null && file.status != 'done'" :data-file="file" @click="onFileClick(index)">
  11. <template v-if="file.status == 'loading'">
  12. <l-loading class="l-upload__progress-loading" size="24px" color="white" />
  13. <text class="l-upload__progress-text" v-if="file.percent != null">{{file.percent}}%</text>
  14. <text class="l-upload__progress-text" v-else>{{loadingText}}</text>
  15. </template>
  16. <l-icon v-else class="l-upload__progress-icon"
  17. :name="file.status == 'reload' ? 'refresh' : 'close-circle'" size="48rpx" aria-hidden />
  18. <text v-if="file.status == 'reload' || file.status == 'failed'" class="l-upload__progress-text">
  19. {{file.status == 'reload' ? reloadText : failedText}}
  20. </text>
  21. </view>
  22. <view class="l-upload__delete-btn" aria-role="button" aria-label="删除" :data-index="index"
  23. @click="onDelete(index)">
  24. <l-icon name="close" size="16px" color="#fff" />
  25. </view>
  26. </slot>
  27. </view>
  28. </view>
  29. <view class="l-upload__item l-upload__item--add"
  30. :class="{'l-upload__item--disabled':disabled}"
  31. :style="[itemStyle]"
  32. aria-label="上传"
  33. v-show="!multiple ? customFiles.length == 0: true"
  34. @click="onAddTap">
  35. <view class="l-upload__item-inner" :style="[innerStyle, {background: addBgColor}]">
  36. <slot>
  37. <l-icon class="l-upload__add-icon" :size="uploadIconSize" :name="uploadIcon" />
  38. </slot>
  39. </view>
  40. </view>
  41. </view>
  42. </template>
  43. <script lang="ts">
  44. // @ts-nocheck
  45. import { defineComponent, ref, computed, watch, onUnmounted } from '@/uni_modules/lime-shared/vue';
  46. import { unitConvert } from '@/uni_modules/lime-shared/unitConvert'
  47. import uploadProps from './props'
  48. import { chooseFiles } from './utils'
  49. import { UploadFile, ChooseFileOptions } from './type';
  50. export default defineComponent({
  51. name: 'l-upload',
  52. props: uploadProps,
  53. emit: ['fail', 'remove', 'success', 'click', 'add', 'input', 'update:modelValue'],
  54. setup(props, {expose, emit}){
  55. const customFiles = ref<UploadFile[]>(props.value ?? props.modelValue ?? props.defaultFiles ?? []);
  56. const styles = computed(()=> {
  57. const style:Record<string, string> = {}
  58. const gutter = unitConvert(props.gutter ?? 8) / 2 * -1;
  59. style['margin-left'] = `${gutter}px`;
  60. style['margin-right'] = `${gutter}px`;
  61. style['margin-top'] = `${gutter}px`;
  62. return style
  63. })
  64. const itemStyle = computed(() => {
  65. const style:Record<string, string> = {}
  66. let column = props.column ?? 4;
  67. if(props.column) {
  68. style['width'] = `${100 / column}%`
  69. }
  70. if(props.gridWidth) {
  71. const gutter = unitConvert(props.gutter ?? 8)
  72. style['width'] = `${unitConvert(props.gridWidth) + gutter}px`
  73. }
  74. return style
  75. })
  76. const innerStyle = computed(()=>{
  77. const style:Record<string, string> = {};
  78. const gutter = unitConvert(props.gutter ?? 8) / 2
  79. style['margin'] = `${gutter}px`
  80. // if(props.gridWidth) {
  81. // style['width'] = props.gridWidth
  82. // }
  83. if(props.gridBgColor){
  84. style.set('background', props.gridBgColor!)
  85. }
  86. if(props.gridHeight) {
  87. style['height'] = props.gridHeight
  88. }
  89. return style
  90. })
  91. const onFileClick = (index : number) => {
  92. const file = customFiles.value[index]
  93. emit('click', { file })
  94. }
  95. const onProofTap = (index : number) => {
  96. onFileClick(index);
  97. if (props.disablePreview) return
  98. uni.previewImage({
  99. urls: customFiles.value.filter((file) : boolean => file.percent != -1).map((file) : string => file.url),
  100. current: index
  101. });
  102. }
  103. const onDelete = (index : number) => {
  104. const delFile = customFiles.value[index]
  105. customFiles.value = customFiles.value.filter((file, i) : boolean => index != i)
  106. emit('remove', { index, file: delFile })
  107. }
  108. let last = 0;
  109. const upload = (files: UploadFile[]) => {
  110. if(!props.autoUpload || props.action == null || props.action!.length < 5) return
  111. if(props.action == 'uniCloud') {
  112. let uploadImgs:Promise<UniCloudUploadFileResult>[] = [];
  113. files.forEach((file, index) =>{
  114. const promise = new Promise<UniCloudUploadFileResult>((resolve, reject) =>{
  115. uniCloud.uploadFile({
  116. filePath: file.url,
  117. cloudPath: file.name!.substring(file.name!.lastIndexOf('.')),
  118. onUploadProgress: (res)=>{
  119. file.status = 'loading'
  120. file.percent = Math.floor(res.loaded / res.total * 100)
  121. },
  122. }).then(res=>{
  123. file.path = res.fileID;
  124. file.status = 'done'
  125. resolve(res)
  126. }).catch(err=>{
  127. file.status = 'failed'
  128. reject(err)
  129. })
  130. })
  131. uploadImgs.push(promise as Promise<UniCloudUploadFileResult>)
  132. })
  133. Promise.all(uploadImgs).then(res =>{
  134. emit('success', res)
  135. }).catch(err => {
  136. emit('fail', err)
  137. })
  138. } else {
  139. let uploadImgs:Promise<UploadFileSuccess>[] = [];
  140. files.forEach((file, index) =>{
  141. const promise = new Promise<UploadFileSuccess>((resolve, reject) =>{
  142. const task = uni.uploadFile({
  143. url: props.action!,
  144. filePath: file.url,
  145. name: file.name,
  146. formData: props.formData,
  147. header: props.headers,
  148. success: (res) => {
  149. file.status = 'done'
  150. if(res.statusCode == 200) {
  151. if(typeof res.data == 'string') {
  152. try{
  153. const data = JSON.parse<UTSJSONObject>(res.data);
  154. const url = data?.getString('url');
  155. if(url != null) {
  156. file.path = url
  157. }
  158. }catch(e){
  159. //TODO handle the exception
  160. }
  161. }
  162. }
  163. resolve(res)
  164. },
  165. fail(err) {
  166. file.status = 'failed'
  167. reject(err)
  168. }
  169. });
  170. task.onProgressUpdate((res) => {
  171. file.status = 'loading'
  172. file.percent = res.progress
  173. });
  174. })
  175. uploadImgs.push(promise as Promise<UploadFileSuccess>)
  176. })
  177. Promise.all(uploadImgs).then(res =>{
  178. emit('success', res)
  179. }).catch(err => {
  180. emit('fail', err)
  181. })
  182. }
  183. };
  184. const customLimit = computed(() : number => props.max == 0 ? 20 : props.max - customFiles.value.length)
  185. const onAddTap = () => {
  186. if (props.disabled) return;
  187. chooseFiles({
  188. mediaType: props.mediaType,
  189. count: customLimit.value,
  190. sourceType: props.sourceType,
  191. sizeType: props.sizeType,
  192. sizeLimit: props.sizeLimit
  193. } as ChooseFileOptions).then((files) =>{
  194. last = customFiles.value.length
  195. customFiles.value = customFiles.value.concat(files)
  196. const _files = customFiles.value.filter((it, i):boolean => i > last - 1)
  197. upload(_files)
  198. emit('add', _files)
  199. })
  200. }
  201. const stop = watch(customFiles, (v : UploadFile[]) => {
  202. emit('update:modelValue', v)
  203. // #ifdef VUE2
  204. emit('input', v)
  205. // #endif
  206. })
  207. // #ifdef VUE2
  208. const stopValue = watch(() => props.value, (v: UploadFile[])=>{
  209. if(v.length != customFiles.value.length){
  210. customFiles.value = v
  211. }
  212. })
  213. // #endif
  214. // #ifdef VUE3
  215. const stopValue = watch(() => props.modelValue, (v: UploadFile[])=>{
  216. if(v.length != customFiles.value.length){
  217. customFiles.value = v
  218. }
  219. })
  220. // #endif
  221. // #ifdef VUE3
  222. expose({
  223. remove: onDelete
  224. });
  225. // #endif
  226. onUnmounted(() => {
  227. stopValue()
  228. stop()
  229. })
  230. return {
  231. styles,
  232. customFiles,
  233. itemStyle,
  234. innerStyle,
  235. onProofTap,
  236. onDelete,
  237. onAddTap,
  238. onFileClick,
  239. // #ifdef VUE2
  240. remove: onDelete,
  241. // #endif
  242. }
  243. }
  244. })
  245. </script>
  246. <style lang="scss">
  247. @import './index';
  248. </style>