| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838 |
- <template>
- <div class="container">
- <van-nav-bar
- title="图片编辑"
- left-arrow
- @click-left="goBack"
- @click-right="onReset"
- >
- <template #left>
- <van-icon name="arrow-left" size="25" />
- <div class="left-btn">返回</div>
- </template>
- <template #right>
- <div class="nav-right right-btn">重置</div>
- </template>
- </van-nav-bar>
- <div class="init-container">
- <div class="workbench-info">
- <p class="info-line">
- 仓库:<span class="info-value">{{ workbench.warehouseCode }}</span>
- </p>
- <p class="info-line">
- 工作台:<span class="info-value">{{ workbench.workStation }}</span>
- </p>
- <p class="info-line hint-text">支持png/jpeg/jpg/webp/heic/heif(heic/heif自动转webp)</p>
- <van-button
- size="mini"
- type="primary"
- plain
- icon="replay"
- class="refresh-btn"
- @click.stop="getWorkbench"
- ></van-button>
- </div>
- <div class="scan-returned-content">
- <div class="input-group">
- <van-cell title="外箱图上传" />
- <van-uploader
- v-model="outerImages"
- :max-count="5"
- :max-size="15 * 1024 * 1024"
- :before-read="beforeReadImage"
- :preview-full-image="true"
- :deletable="true"
- >
- <template #preview-cover="{ file, index }">
- <div class="custom-cover">
- <!-- 状态指示器 -->
- <div class="upload-status" :class="file.status">
- <span v-if="file.status === 'uploading'">上传中...</span>
- <span v-else-if="file.status === 'retrying'">重试中...</span>
- <span v-else-if="file.status === 'success'">✓ 成功</span>
- <span v-else-if="file.status === 'failed'">
- ✗ 失败
- <van-button
- v-if="file.retryCount < 1"
- size="mini"
- @click.stop="retrySingleImage(file, 'RETURNED_BOX_IMAGE')"
- style="margin-left: 5px; background: #fff; color: #ee0a24"
- >
- 重试
- </van-button>
- <span v-else style="margin-left: 5px">(已达上限)</span>
- </span>
- </div>
- <van-button
- class="cover-button"
- size="mini"
- icon="edit"
- @click.stop="handleOuterImages(file, index)"
- >
- 编辑
- </van-button>
- </div>
- </template>
- </van-uploader>
- </div>
- <div class="input-group">
- <van-cell title="内物图上传" />
- <van-uploader
- v-model="innerImages"
- :max-count="5"
- :max-size="15 * 1024 * 1024"
- :before-read="beforeReadImage"
- :preview-full-image="true"
- :deletable="true"
- >
- <template #preview-cover="{ file, index }">
- <div class="custom-cover">
- <!-- 状态指示器 -->
- <div class="upload-status" :class="file.status">
- <span v-if="file.status === 'uploading'">上传中...</span>
- <span v-else-if="file.status === 'retrying'">重试中...</span>
- <span v-else-if="file.status === 'success'">✓ 成功</span>
- <span v-else-if="file.status === 'failed'">
- ✗ 失败
- <van-button
- v-if="file.retryCount < 1"
- size="mini"
- @click.stop="
- retrySingleImage(file, 'RETURNED_INNER_IMAGE')
- "
- style="margin-left: 5px; background: #fff; color: #ee0a24"
- >
- 重试
- </van-button>
- <span v-else style="margin-left: 5px">(已达上限)</span>
- </span>
- </div>
- <van-button
- class="cover-button"
- size="mini"
- icon="edit"
- @click.stop="handleInnerImages(file, index)"
- >
- 编辑
- </van-button>
- </div>
- </template>
- </van-uploader>
- </div>
- <div class="button-group">
- <van-button class="action-btn" type="warning" plain @click="previewAll">预览全部</van-button>
- <van-button class="action-btn action-btn--danger" type="danger" plain @click="onReset">重置图片</van-button>
- <van-button
- class="action-btn"
- type="primary"
- plain
- @click="retryAllFailedImages"
- :disabled="!hasFailedImages()"
- >重传失败图片</van-button>
- <van-button class="action-btn" type="primary" @click="onSubmit">提交</van-button>
- </div>
- </div>
- </div>
- <!-- 自定义图片预览组件 -->
- <van-image-preview
- v-model:show="previewVisible"
- :images="previewImages"
- :start-position="previewStartPosition"
- @change="handlePreviewChange"
- >
- <!-- 自定义操作按钮 -->
- <template #index>
- <div class="preview-toolbar">
- <van-button
- type="primary"
- size="small"
- @click="onEditImage(previewCurrentIndex)"
- :disabled="
- !canEditImage(getPreviewImage(previewCurrentIndex))
- "
- >
- 编辑
- </van-button>
- </div>
- </template>
- </van-image-preview>
- <edit-image ref="editImageRef" @save-image="changeFile" />
- </div>
- </template>
- <script setup lang="ts">
- import { ref, onMounted } from 'vue'
- // Type declarations for image upload handling
- enum UPLOAD_STATUS {
- PENDING = 'pending',
- UPLOADING = 'uploading',
- SUCCESS = 'success',
- FAILED = 'failed',
- RETRYING = 'retrying',
- }
- interface UploadImage {
- file: File
- status: UPLOAD_STATUS
- url: string | null
- error: string | null
- retryCount: number
- originalFile: File
- // Optional; some flows may carry raw content for previews
- content?: string
- }
- interface Workbench {
- warehouseCode: string
- workStation: string
- }
- interface EditImageExposed {
- editImage: (image: UploadImage, type: 'outer' | 'inner') => void
- }
- import {
- showFailToast,
- showNotify,
- showLoadingToast,
- closeToast,
- showConfirmDialog,
- } from 'vant'
- import { getHeader, goBack } from '@/utils/android'
- import { detailImageUpload, returnedWorkbench } from '@/api/returned/index.ts'
- import { compressImage } from '@/utils/imageCompression'
- import { convertHeicHeifToWebp, isHeicOrHeif } from '@/utils/imageFormat'
- import EditImage from '@/components/EditImage.vue'
- const workbench = ref<Workbench>({ warehouseCode: '', workStation: '' })
- onMounted(async () => {
- try {
- await getHeader()
- } catch (error: any) {
- if (typeof showFailToast === 'function') {
- showFailToast(error?.message || '获取头信息失败,请稍后重试')
- }
- console.error(error)
- }
- getWorkbench()
- })
- // 图片上传相关 - 增强数据结构
- const outerImages = ref<UploadImage[]>([]) // 外箱图 [{file, status, url, error, retryCount, originalFile}]
- const innerImages = ref<UploadImage[]>([]) // 内物图 [{file, status, url, error, retryCount, originalFile}]
- // 预览相关变量
- const previewVisible = ref<boolean>(false)
- const previewImages = ref<string[]>([])
- const previewStartPosition = ref<number>(0)
- const previewCurrentIndex = ref<number>(0)
- // 编辑权限检查
- const canEditImage = (imageObj: UploadImage | undefined): boolean => {
- if (!imageObj) return false
- // 只有上传中和重试中的图片不能编辑
- return (
- imageObj.status !== UPLOAD_STATUS.UPLOADING &&
- imageObj.status !== UPLOAD_STATUS.RETRYING
- )
- }
- // 预览索引变更处理
- const handlePreviewChange = (index: number): void => {
- previewCurrentIndex.value = index
- }
- function getWorkbench(): void {
- returnedWorkbench()
- .then((res) => {
- workbench.value = res.data
- })
- .catch((error: any) => {
- // 记录错误并提示用户,避免静默失败
- console.error('getWorkbench error', error)
- showFailToast('获取工作台信息失败,请稍后重试')
- })
- }
- const beforeReadImage = async (file: File | File[]): Promise<UploadImage[] | false> => {
- const files = Array.isArray(file) ? file : [file]
- const normalizedFiles: File[] = []
- for (const f of files) {
- const isHeicFile = isHeicOrHeif(f)
- const isImage = /^image\//.test(f.type) || isHeicFile
- if (!isImage) {
- showFailToast('仅支持图片文件')
- return false
- }
- if (f.size > 15 * 1024 * 1024) {
- showFailToast('图片大小不能超过15MB')
- return false
- }
- if (isHeicFile) {
- try {
- const convertedFile = await convertHeicHeifToWebp(f)
- normalizedFiles.push(convertedFile)
- } catch (error) {
- console.error('HEIC/HEIF 转换失败:', error)
- showFailToast('HEIC/HEIF 转换失败,请使用 JPG/PNG/WEBP 格式')
- return false
- }
- continue
- }
- normalizedFiles.push(f)
- }
- // 返回增强的图片对象数组,包含状态信息
- return normalizedFiles.map((f) => ({
- file: f, // 原始File对象
- status: UPLOAD_STATUS.PENDING,
- url: null,
- error: null,
- retryCount: 0,
- originalFile: f, // 保留用于重传
- }))
- }
- // 单张图片上传函数
- const uploadSingleImage = async (
- imageObj: UploadImage,
- category: string,
- ): Promise<boolean> => {
- imageObj.status = UPLOAD_STATUS.UPLOADING
- try {
- const compressedFile = await compressImage(imageObj.file, 0.5)
- const data = new FormData()
- data.set('file', compressedFile)
- data.set('type', category)
- // detailImageUpload成功执行即成功,catch到异常即失败
- await detailImageUpload(data)
- imageObj.status = UPLOAD_STATUS.SUCCESS
- imageObj.url = URL.createObjectURL(imageObj.file)
- imageObj.error = null
- return true
- } catch (error) {
- // 标记失败并记录错误,但不要将内部错误信息暴露给用户
- imageObj.status = UPLOAD_STATUS.FAILED
- imageObj.error = '上传失败,请稍后再试'
- // retryCount 不在此处修改,由重试调用方负责递增
- // 记录详细错误以供调试
- console.error('uploadSingleImage error', error)
- return false
- }
- }
- const uploadCategoryWithStatus = async (
- list: UploadImage[],
- categoryName: string,
- ): Promise<{ success: boolean; item: UploadImage }[]> => {
- const results: { success: boolean; item: UploadImage }[] = []
- for (const item of list) {
- // 跳过已成功的图片
- if (item.status === UPLOAD_STATUS.SUCCESS) {
- results.push({ success: true, item })
- continue
- }
- console.log(item,categoryName)
- const success = await uploadSingleImage(item, categoryName)
- results.push({ success, item })
- }
- return results
- }
- // 获取预览图片对象
- const getPreviewImage = (index: number): UploadImage => {
- const allImages = [...outerImages.value, ...innerImages.value]
- return allImages[index] as UploadImage
- }
- const onEditImage = (index: number): void => {
- // 获取图片对象
- const imageObj = getPreviewImage(index)
- if (!imageObj) return
- // 检查是否可以编辑
- if (!canEditImage(imageObj)) {
- showNotify({
- type: 'warning',
- message: '图片正在上传中,请稍后再编辑',
- })
- return
- }
- // 关闭预览
- previewVisible.value = false
- // 确定图片类型和索引
- let type: 'outer' | 'inner'
- if (index < outerImages.value.length) {
- // 是外箱图
- type = 'outer'
- outerImagesIndex.value = index
- } else {
- // 是内物图
- type = 'inner'
- innerImagesIndex.value = index - outerImages.value.length
- }
- // 打开编辑界面
- setTimeout(() => {
- editImageRef.value?.editImage(imageObj, type)
- }, 300)
- }
- const previewAll = (): void => {
- const images = [...outerImages.value, ...innerImages.value]
- .map(
- (it) => it.url || (it.file ? URL.createObjectURL(it.file) : it.content),
- )
- .filter(Boolean)
- if (!images.length) {
- showNotify({ type: 'warning', message: '暂无可预览的图片' })
- return
- }
- previewImages.value = images
- previewStartPosition.value = 0
- previewCurrentIndex.value = 0
- previewVisible.value = true
- }
- const onReset = (): void => {
- outerImages.value = []
- innerImages.value = []
- showNotify({ type: 'primary', message: '已重置图片' })
- }
- const onSubmit = async (): Promise<void> => {
- if (!outerImages.value.length && !innerImages.value.length) {
- showFailToast('请先上传外箱图或内物图')
- return
- }
- try {
- const outerCount = outerImages.value.length
- const innerCount = innerImages.value.length
- await showConfirmDialog({
- title: '确认提交',
- message: `外箱图:${outerCount} 张\n内物图:${innerCount} 张\n是否确认提交?`,
- })
- } catch (e) {
- showNotify({ type: 'warning', message: '已取消提交' })
- return
- }
- const toast = showLoadingToast({ duration: 0, message: '上传中...' })
- try {
- // 使用新的上传函数
- const outerResults = await uploadCategoryWithStatus(
- outerImages.value,
- 'RETURNED_BOX_IMAGE',
- )
- const innerResults = await uploadCategoryWithStatus(
- innerImages.value,
- 'RETURNED_INNER_IMAGE',
- )
- closeToast()
- // 统计结果
- const allResults = [...outerResults, ...innerResults]
- const successCount = allResults.filter((r) => r.success).length
- const failedCount = allResults.filter((r) => !r.success).length
- if (failedCount === 0) {
- // 全部成功:清空所有图片
- showNotify({ type: 'success', message: '提交成功' })
- outerImages.value = []
- innerImages.value = []
- } else {
- // 有失败图片:只清空成功的,保留失败的
- showNotify({
- type: 'warning',
- duration: 5000,
- message: `上传完成,成功 ${successCount} 张,失败 ${failedCount} 张。失败图片已保留,可手动重传。`,
- })
- // 只清空成功的图片
- outerImages.value = outerImages.value.filter(
- (img) => img.status !== UPLOAD_STATUS.SUCCESS,
- )
- innerImages.value = innerImages.value.filter(
- (img) => img.status !== UPLOAD_STATUS.SUCCESS,
- )
- }
- } catch (error) {
- // 保证用户看到的错误信息不包含内部实现细节
- closeToast()
- console.error('onSubmit error', error)
- showFailToast('提交失败,请稍后再试')
- }
- }
- // 手动重传相关函数
- // 单张图片重传
- const retrySingleImage = async (
- imageObj: UploadImage,
- category: string,
- ): Promise<void> => {
- // 检查重传次数
- if (imageObj.retryCount >= 1) {
- showNotify({ type: 'warning', message: '已达到最大重传次数' })
- return
- }
- imageObj.status = UPLOAD_STATUS.RETRYING
- const success = await uploadSingleImage(imageObj, category)
- imageObj.retryCount++ // 无论成功失败,重试次数+1
- if (success) {
- showNotify({ type: 'success', message: '重传成功' })
- // 成功后会从列表中移除(在下次提交时)
- } else {
- showNotify({
- type: 'danger',
- message: `重传失败`,
- })
- }
- }
- // 批量重传失败图片
- const retryAllFailedImages = async (): Promise<void> => {
- const allImages = [...outerImages.value, ...innerImages.value]
- const failedImages = allImages.filter(
- (img) => img.status === UPLOAD_STATUS.FAILED && img.retryCount < 1,
- )
- if (failedImages.length === 0) {
- showNotify({ type: 'primary', message: '没有需要重传的图片' })
- return
- }
- const toast = showLoadingToast({
- duration: 0,
- message: `重传 ${failedImages.length} 张图片...`,
- })
- let successCount = 0
- for (const img of failedImages) {
- const category = outerImages.value.includes(img)
- ? 'RETURNED_BOX_IMAGE'
- : 'RETURNED_INNER_IMAGE'
- const success = await uploadSingleImage(img, category)
- img.retryCount++ // 无论成功失败,重试次数+1
- if (success) successCount++
- }
- closeToast()
- showNotify({
- type: successCount === failedImages.length ? 'success' : 'warning',
- message: `重传完成,成功 ${successCount}/${failedImages.length} 张`,
- })
- }
- // 计算是否有失败图片
- const hasFailedImages = (): boolean => {
- const allImages = [
- ...outerImages.value,
- ...innerImages.value,
- ] as UploadImage[]
- return allImages.some(
- (img: UploadImage) =>
- img.status === UPLOAD_STATUS.FAILED && img.retryCount < 1,
- )
- }
- // 外箱图
- const outerImagesIndex = ref<number>(-1)
- // 内物图
- const innerImagesIndex = ref<number>(-1)
- const editImageRef = ref<EditImageExposed | null>(null)
- /**
- * 处理图片编辑操作
- * @param file 文件对象
- * @param index 文件下标
- */
- const handleInnerImages = (_slotFile: unknown, index: number): void => {
- // 直接从响应式列表取原始对象,避免 Vant 插槽包装层导致 file 字段异常
- const imageObj = innerImages.value[index]
- if (!imageObj) return
- if (!canEditImage(imageObj)) {
- showNotify({
- type: 'warning',
- message: '图片正在上传中,请稍后再编辑',
- })
- return
- }
- // 保证传入 EditImage 时有可用的预览 URL
- if (!imageObj.url && !imageObj.content && imageObj.file instanceof File) {
- imageObj.content = URL.createObjectURL(imageObj.file)
- }
- innerImagesIndex.value = index
- editImageRef.value?.editImage(imageObj, 'inner')
- }
- /**
- * 处理图片编辑操作
- * @param _slotFile 插槽传来的对象(Vant 包装层,不可靠,弃用)
- * @param index 文件下标
- */
- const handleOuterImages = (_slotFile: unknown, index: number): void => {
- // 直接从响应式列表取原始对象,避免 Vant 插槽包装层导致 file 字段异常
- const imageObj = outerImages.value[index]
- if (!imageObj) return
- if (!canEditImage(imageObj)) {
- showNotify({
- type: 'warning',
- message: '图片正在上传中,请稍后再编辑',
- })
- return
- }
- // 保证传入 EditImage 时有可用的预览 URL
- if (!imageObj.url && !imageObj.content && imageObj.file instanceof File) {
- imageObj.content = URL.createObjectURL(imageObj.file)
- }
- outerImagesIndex.value = index
- editImageRef.value?.editImage(imageObj, 'outer')
- }
- const changeFile = async (
- editedDataURL: string,
- type: 'inner' | 'outer',
- ): Promise<void> => {
- try {
- // 根据type找到对应的图片索引和列表
- const index =
- type === 'inner' ? innerImagesIndex.value : outerImagesIndex.value
- const imageList = type === 'inner' ? innerImages.value : outerImages.value
- if (index === -1 || !imageList[index]) {
- showNotify({ type: 'warning', message: '未找到对应的图片' })
- return
- }
- const imageObj = imageList[index]
- // 将base64转换为File对象
- const editedFile = dataURLtoFile(
- editedDataURL,
- getFileNameByMime(imageObj.file.name, editedDataURL),
- )
- // 压缩图片(EditImage保存的是PNG,可能很大)
- const compressedFile = await compressImage(editedFile)
- // 释放旧的预览URL(内存管理)
- if (imageObj.url && imageObj.url.startsWith('blob:')) {
- URL.revokeObjectURL(imageObj.url)
- }
- // 只更新文件内容和预览URL
- imageObj.file = compressedFile
- imageObj.originalFile = compressedFile
- imageObj.url = editedDataURL // 使用原始的base64作为预览(质量更好)
- // 若图片已成功上传,编辑后需重置为 PENDING,确保提交时重新上传编辑后的版本
- if (imageObj.status === UPLOAD_STATUS.SUCCESS) {
- imageObj.status = UPLOAD_STATUS.PENDING
- imageObj.error = null
- }
- // 其余状态(PENDING / FAILED)保持不变,由正常上传流程处理
- showNotify({ type: 'success', message: '图片编辑完成' })
- } catch (error) {
- console.error('图片编辑保存失败:', error)
- showNotify({
- type: 'danger',
- message: '图片编辑保存失败,请重试',
- })
- }
- }
- // base64转File函数
- function dataURLtoFile(dataurl: string, filename: string): File {
- const arr = dataurl.split(',')
- const mimeMatch = arr[0].match(/:(.*?);/)
- if (!mimeMatch) throw new Error('无效的 dataURL 格式')
- const mime = mimeMatch[1]
- const bstr = atob(arr[1])
- let n = bstr.length
- const u8arr = new Uint8Array(n)
- while (n--) {
- u8arr[n] = bstr.charCodeAt(n)
- }
- return new File([u8arr], filename, { type: mime })
- }
- function getFileNameByMime(fileName: string, dataUrl: string): string {
- const mimeMatch = dataUrl.match(/^data:(.*?);/)
- const mime = mimeMatch?.[1] || ''
- const ext = mime.split('/')[1]
- if (!ext) return fileName
- const normalizedExt = ext.split('+')[0].toLowerCase()
- const dotIndex = fileName.lastIndexOf('.')
- if (dotIndex <= 0) return `${fileName}.${normalizedExt}`
- return `${fileName.slice(0, dotIndex)}.${normalizedExt}`
- }
- </script>
- <style scoped lang="sass">
- .van-nav-bar
- .left-btn
- color: #fff
- height: 46px
- padding-right: 20px
- line-height: 46px
- .right-btn
- color: #fff
- // ── 仓库信息区 ──
- .workbench-info
- background: #fff
- padding: 10px 14px 8px
- border-bottom: 1px solid #f0f0f0
- .info-line
- margin: 0 0 4px
- font-size: 14px
- color: #666
- .info-value
- color: #1989fa
- font-weight: 500
- .hint-text
- color: #999
- font-size: 12px
- .refresh-btn
- margin-top: 4px
- // ── 主内容区 ──
- .container
- .init-container
- width: 100%
- .scan-returned-content
- padding: 10px
- .input-group
- padding: 5px
- .button-group
- display: flex
- gap: 8px
- padding: 8px 5px
- flex-wrap: wrap
- .action-btn
- flex: 1
- min-width: 70px
- height: 38px
- font-size: 13px
- border-radius: 8px
- .content
- width: 100%
- .scan-returned-no
- align-items: center
- padding: 15px
- .returned-detail-list
- .card-div
- background: #fff
- border-radius: 12px
- overflow: hidden
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05)
- margin: 5px 0
- padding: 5px 0
- .card-div-content
- padding: 3px
- .info-row
- display: flex
- margin-bottom: 12px
- font-size: 12px
- // 图片预览工具栏样式
- .preview-toolbar
- display: flex
- justify-content: flex-end
- margin-top: 10px
- padding: 0 15px
- .van-button
- margin-left: 10px
- .custom-cover
- position: absolute
- top: 0
- left: 0
- box-sizing: border-box
- width: 100%
- height: 100%
- display: flex
- align-items: flex-end
- /* 按钮通常放在底部 */
- justify-content: center
- padding: 8px
- /* 留出边距 */
- background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent)
- /* 可选:底部渐变背景,让按钮更清晰 */
- pointer-events: none
- /* 关键:避免覆盖层拦截图片的点击预览事件 */
- .cover-button
- pointer-events: auto
- /* 关键:允许按钮本身接收点击事件 */
- // 上传状态样式
- .upload-status
- position: absolute
- top: 5px
- left: 5px
- padding: 2px 6px
- border-radius: 3px
- font-size: 12px
- color: white
- background-color: rgba(0, 0, 0, 0.7)
- .upload-status.success
- background-color: #07c160
- .upload-status.failed
- background-color: #ee0a24
- .upload-status.uploading
- background-color: #1989fa
- .upload-status.retrying
- background-color: #ff976a
- .upload-status.pending
- background-color: #969799
- </style>
|