合同小程序前端代码仓库
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.

322 lines
10 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. <!-- #ifndef APP || WEB -->
  5. <view class="l-upload__item-inner" :style="[innerStyle]">
  6. <!-- #endif -->
  7. <slot name="file" :file="file" :index="index">
  8. <image class="l-upload__image" v-if="file.type == 'image'" :src="file.url" :data-file="file"
  9. :mode="imageFit" @click="onProofTap(index)" />
  10. <video class="l-upload__image" v-if="file.type == 'video'" :src="file.url" :data-file="file"
  11. :autoplay="false" objectFit="contain" @click="onFileClick(index)" />
  12. <view class="l-upload__progress-mask" v-if="file.status != null && file.status != 'done'" :data-file="file" @click="onFileClick(index)">
  13. <template v-if="file.status == 'loading'">
  14. <l-loading class="l-upload__progress-loading" size="24px" color="white" />
  15. <text class="l-upload__progress-text" v-if="file.percent != null">{{file.percent}}%</text>
  16. <text class="l-upload__progress-text" v-else>{{loadingText}}</text>
  17. </template>
  18. <l-icon v-else class="l-upload__progress-icon"
  19. :name="file.status == 'reload' ? 'refresh' : 'close-circle'" size="48rpx" aria-hidden />
  20. <text v-if="file.status == 'reload' || file.status == 'failed'" class="l-upload__progress-text">
  21. {{file.status == 'reload' ? reloadText : failedText}}
  22. </text>
  23. </view>
  24. <view class="l-upload__delete-btn" aria-role="button" aria-label="删除" :data-index="index"
  25. @click="onDelete(index)">
  26. <l-icon name="close" size="16px" color="#fff" />
  27. </view>
  28. </slot>
  29. <!-- #ifndef APP || WEB -->
  30. </view>
  31. <!-- #endif -->
  32. </view>
  33. <!-- #ifdef APP || WEB -->
  34. <view class="l-upload__item l-upload__item--add"
  35. :class="{'l-upload__item--disabled':disabled}"
  36. v-show="!multiple ? customFiles.length == 0: true"
  37. :style="[itemStyle, addBgColor!=null ? {background: addBgColor}: '']"
  38. aria-label="上传"
  39. @click="onAddTap">
  40. <slot>
  41. <l-icon class="l-upload__add-icon" :size="uploadIconSize" :name="uploadIcon" />
  42. </slot>
  43. </view>
  44. <!-- #endif -->
  45. <!-- #ifndef APP || WEB -->
  46. <view class="l-upload__item l-upload__item--add"
  47. :class="{'l-upload__item--disabled':disabled}"
  48. v-show="!multiple ? customFiles.length == 0: true"
  49. :style="[itemStyle]"
  50. aria-label="上传"
  51. @click="onAddTap">
  52. <view class="l-upload__item-inner" :style="[innerStyle, addBgColor!=null ? {background: addBgColor}: '']">
  53. <slot>
  54. <l-icon class="l-upload__add-icon" :size="uploadIconSize" :name="uploadIcon" />
  55. </slot>
  56. </view>
  57. </view>
  58. <!-- #endif -->
  59. </view>
  60. </template>
  61. <script lang="uts" setup>
  62. import { unitConvert } from '@/uni_modules/lime-shared/unitConvert'
  63. import { chooseFiles } from './utils'
  64. import { UploadProps, UploadFile, ChooseFileOptions } from './type';
  65. defineSlots<{
  66. file(props : { file : UploadFile, index: number }) : any,
  67. }>()
  68. const emits = defineEmits(['fail', 'remove', 'success', 'click', 'add', 'update:modelValue'])
  69. const props = withDefaults(defineProps<UploadProps>(), {
  70. imageFit: 'aspectFill',
  71. disablePreview: false,
  72. autoUpload: false,
  73. multiple: true,
  74. max: 0,
  75. disabled: false,
  76. mediaType: 'image',
  77. sizeType: ['original', 'compressed'],
  78. sourceType: ['album', 'camera'],
  79. uploadIcon: 'camera',
  80. loadingText: '上传中...',
  81. reloadText: '重新上传',
  82. failedText: '上传失败',
  83. mode: 'grid'
  84. })
  85. const transformFiles = (it : any) : UploadFile => {
  86. // #ifdef APP-ANDROID
  87. if(it instanceof UploadFile) {
  88. return it
  89. }
  90. // #endif
  91. // #ifndef APP-ANDROID || APP-IOS
  92. const file:UTSJSONObject = {...it}
  93. // #endif
  94. // #ifdef APP-ANDROID || APP-IOS
  95. const file = UTSJSONObject.assign({}, it as UTSJSONObject) //it as UTSJSONObject
  96. // #endif
  97. return {
  98. url: file.getString('url') ?? '',
  99. path: file.getString('path'),
  100. name: file.getString('name'),
  101. thumb: file.getString('thumb'),
  102. size: file.getNumber('size'),
  103. type: file.getString('type'),
  104. percent: file.getNumber('percent'),
  105. status: file.getString('status') ?? 'done',
  106. } as UploadFile
  107. }
  108. const customFiles = ref<UploadFile[]>((props.modelValue ?? props.defaultFiles)?.map(transformFiles) ?? []);
  109. const listWidth = ref(0);
  110. const uploadRef = ref<UniElement | null>(null);
  111. const styles = computed(() : Map<string, any>=> {
  112. const style = new Map<string, any>();
  113. const gutter = unitConvert(props.gutter ?? 8) / 2 * -1
  114. style.set('margin-left', `${gutter}px`)
  115. style.set('margin-right', `${gutter}px`)
  116. style.set('margin-top', `${gutter}px`)
  117. return style
  118. })
  119. const itemStyle = computed(() : Map<string, any> => {
  120. const style = new Map<string, any>();
  121. // const gridWidth = unitConvert(props.gridWidth ?? 80)
  122. const gutter = unitConvert(props.gutter ?? 8) / 2
  123. let column = props.column ?? 4;
  124. if(props.gridWidth != null || props.column != null) {
  125. // #ifdef APP || WEB
  126. const width = listWidth.value / column - gutter * 2.0275;// ios 计算精度导致不均分
  127. style.set('width', props.gridWidth ?? `${width}px`)
  128. // #endif
  129. // #ifndef APP || WEB
  130. style.set('width', props.gridWidth ?? `${100 / column}%`)
  131. // #endif
  132. }
  133. // #ifdef APP || WEB
  134. if(props.gridHeight != null){
  135. style.set('height', props.gridHeight!)
  136. }
  137. style.set('margin', `${gutter}px`)
  138. if(props.gridBgColor != null){
  139. style.set('background', props.gridBgColor!)
  140. }
  141. // #endif
  142. return style
  143. })
  144. // #ifndef APP || WEB
  145. const innerStyle = computed((): Map<string, any> => {
  146. const style = new Map<string, any>();
  147. const gutter = unitConvert(props.gutter ?? 8) / 2
  148. style.set('margin', `${gutter}px`)
  149. if(props.gridBgColor != null){
  150. style.set('background', props.gridBgColor!)
  151. }
  152. if(props.gridHeight != null){
  153. style.set('height', props.gridHeight!)
  154. }
  155. return style
  156. })
  157. // #endif
  158. const onFileClick = (index : number) => {
  159. const file = customFiles.value[index]
  160. emits('click', { file })
  161. }
  162. const onProofTap = (index : number) => {
  163. onFileClick(index);
  164. if (props.disablePreview) return
  165. uni.previewImage({
  166. urls: customFiles.value.filter((file) : boolean => file.percent != -1).map((file) : string => file.url),
  167. current: index
  168. });
  169. }
  170. const onDelete = (index : number) => {
  171. const delFile = customFiles.value[index]
  172. customFiles.value = customFiles.value.filter((file, i) : boolean => index != i)
  173. emits('remove', { index, file: delFile })
  174. }
  175. let last:number// = 0
  176. const upload = (files: UploadFile[]) => {
  177. if(!props.autoUpload || props.action == null || props.action!.length < 5) return
  178. if(props.action == 'uniCloud') {
  179. let uploadImgs:Promise<UniCloudUploadFileResult>[] = [];
  180. files.forEach((file, index) =>{
  181. // props.beforeRead(file).then((res)=>{})
  182. const promise = new Promise<UniCloudUploadFileResult>((resolve, reject) =>{
  183. uniCloud.uploadFile({
  184. filePath: file.url,
  185. cloudPath: file.name!.substring(file.name!.lastIndexOf('.')),
  186. onUploadProgress: (res)=>{
  187. file.status = 'loading'
  188. file.percent = Math.floor(res.loaded / res.total * 100)
  189. },
  190. }).then(res=>{
  191. file.path = res.fileID;
  192. file.status = 'done'
  193. resolve(res)
  194. }).catch(err=>{
  195. file.status = 'failed'
  196. reject(err)
  197. })
  198. })
  199. uploadImgs.push(promise as Promise<UniCloudUploadFileResult>)
  200. })
  201. Promise.all(uploadImgs).then(res =>{
  202. emits('success', res)
  203. }).catch(err => {
  204. emits('fail', err)
  205. })
  206. } else {
  207. let uploadImgs:Promise<UploadFileSuccess>[] = [];
  208. files.forEach((file, index) =>{
  209. const promise = new Promise<UploadFileSuccess>((resolve, reject) =>{
  210. const task = uni.uploadFile({
  211. url: props.action!,
  212. filePath: file.url,
  213. name: file.name,
  214. formData: props.formData,
  215. header: props.headers,
  216. success: (res) => {
  217. file.status = 'done'
  218. if(res.statusCode == 200) {
  219. if(typeof res.data == 'string') {
  220. try{
  221. const data = JSON.parse<UTSJSONObject>(res.data);
  222. const url = data?.getString('url');
  223. if(url != null) {
  224. file.path = url
  225. }
  226. }catch(e){
  227. //TODO handle the exception
  228. }
  229. }
  230. }
  231. resolve(res)
  232. },
  233. fail(err) {
  234. file.status = 'failed'
  235. reject(err)
  236. }
  237. });
  238. task.onProgressUpdate((res) => {
  239. file.status = 'loading'
  240. file.percent = res.progress
  241. });
  242. })
  243. uploadImgs.push(promise as Promise<UploadFileSuccess>)
  244. })
  245. Promise.all(uploadImgs).then(res =>{
  246. emits('success', res)
  247. }).catch(err => {
  248. emits('fail', err)
  249. })
  250. }
  251. }
  252. const customLimit = computed(() : number => props.max == 0 ? 20 : props.max - customFiles.value.length)
  253. const onAddTap = () => {
  254. if (props.disabled) return;
  255. chooseFiles({
  256. mediaType: props.mediaType,
  257. count: customLimit.value,
  258. sourceType: props.sourceType,
  259. sizeType: props.sizeType,
  260. sizeLimit: props.sizeLimit
  261. } as ChooseFileOptions).then((files) =>{
  262. last = customFiles.value.length
  263. customFiles.value = customFiles.value.concat(files)
  264. // @ts-ignore
  265. const _files = customFiles.value.filter((it, i):boolean => i > last - 1)
  266. upload(_files)
  267. emits('add', _files)
  268. })
  269. }
  270. const stop = watch(customFiles, (v : UploadFile[]) => {
  271. emits('update:modelValue', v)
  272. })
  273. const stopValue = watch(():(UTSJSONObject[] | null) => props.modelValue, (v: UTSJSONObject[]|null)=>{
  274. if(v != null && v.length != customFiles.value.length){
  275. customFiles.value = v.map(transformFiles)
  276. }
  277. })
  278. defineExpose({
  279. remove: onDelete
  280. })
  281. // #ifdef APP || WEB
  282. const resizeObserver = new UniResizeObserver((entries : Array<UniResizeObserverEntry>) => {
  283. listWidth.value = entries[0].target.getBoundingClientRect().width
  284. })
  285. onMounted(() => {
  286. nextTick(() => {
  287. listWidth.value = uploadRef.value?.getBoundingClientRect().width ?? 0;
  288. resizeObserver.observe(uploadRef.value!)
  289. })
  290. })
  291. // #endif
  292. onUnmounted(() => {
  293. stop()
  294. // #ifdef APP
  295. resizeObserver.disconnect()
  296. // #endif
  297. })
  298. </script>
  299. <style lang="scss">
  300. @import './index';
  301. </style>