|
@@ -154,7 +154,7 @@
|
|
|
</view>
|
|
</view>
|
|
|
</wd-popup>
|
|
</wd-popup>
|
|
|
|
|
|
|
|
- <canvas :canvas-id="canvasId" :style="watermarkCanvasStyle" />
|
|
|
|
|
|
|
+ <canvas :id="canvasId" :canvas-id="canvasId" :style="watermarkCanvasStyle" />
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -176,6 +176,8 @@ import type { TaskFieldConfigItem } from '@/services/modules/task/taskFrom/type'
|
|
|
|
|
|
|
|
import { useUserStore } from '@/stores/modules/user'
|
|
import { useUserStore } from '@/stores/modules/user'
|
|
|
|
|
|
|
|
|
|
+import { renderTaskWatermark } from '../composables/taskWatermark'
|
|
|
|
|
+
|
|
|
type AttachmentType = 'image' | 'file'
|
|
type AttachmentType = 'image' | 'file'
|
|
|
type UploadSource = 'camera' | 'album' | 'file'
|
|
type UploadSource = 'camera' | 'album' | 'file'
|
|
|
type SourceSuffix = '' | ';1' | ';2' | ';3' | ';4'
|
|
type SourceSuffix = '' | ';1' | ';2' | ';3' | ';4'
|
|
@@ -225,9 +227,6 @@ const FILE_EXTENSIONS = new Set(['pdf', 'ppt', 'pptx'])
|
|
|
const FILE_EXTENSION_LIST = ['ppt', 'pptx', 'pdf']
|
|
const FILE_EXTENSION_LIST = ['ppt', 'pptx', 'pdf']
|
|
|
const IMAGE_MAX_SIZE = 2 * 1024 * 1024
|
|
const IMAGE_MAX_SIZE = 2 * 1024 * 1024
|
|
|
const FILE_MAX_SIZE = 50 * 1024 * 1024
|
|
const FILE_MAX_SIZE = 50 * 1024 * 1024
|
|
|
-const MAX_WATERMARK_LONG_EDGE = 1920
|
|
|
|
|
-const WATERMARK_JPEG_QUALITY = 0.82
|
|
|
|
|
-const WATERMARK_TIMEOUT_MS = 20000
|
|
|
|
|
const UPLOAD_TIMEOUT_MS = 30000
|
|
const UPLOAD_TIMEOUT_MS = 30000
|
|
|
const uploadLogEnabled = import.meta.env.DEV || import.meta.env.VITE_REQUEST_LOG === 'true'
|
|
const uploadLogEnabled = import.meta.env.DEV || import.meta.env.VITE_REQUEST_LOG === 'true'
|
|
|
const SOURCE_SUFFIX_MAP: Record<UploadSource, SourceSuffix> = {
|
|
const SOURCE_SUFFIX_MAP: Record<UploadSource, SourceSuffix> = {
|
|
@@ -266,7 +265,7 @@ const maxCount = computed(() => {
|
|
|
return toPositiveInteger(props.taskFieldConfig.taskFiledMaxsize, 10)
|
|
return toPositiveInteger(props.taskFieldConfig.taskFiledMaxsize, 10)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-const canvasId = computed(() => `imgAndFileWatermarkCanvas-${props.taskFieldConfig.id}`)
|
|
|
|
|
|
|
+const canvasId = computed(() => `imgAndFileWatermarkCanvas-${instance?.uid}`)
|
|
|
|
|
|
|
|
const watermarkCanvasStyle = computed<CSSProperties>(() => ({
|
|
const watermarkCanvasStyle = computed<CSSProperties>(() => ({
|
|
|
width: `${watermarkCanvasWidth.value}px`,
|
|
width: `${watermarkCanvasWidth.value}px`,
|
|
@@ -327,9 +326,10 @@ const selectAndUploadImages = async (source: 'camera' | 'album'): Promise<void>
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const remainingCount = Math.max(maxCount.value - attachments.value.length, 0)
|
|
const remainingCount = Math.max(maxCount.value - attachments.value.length, 0)
|
|
|
- const files = await chooseImages(Math.min(remainingCount, 9), source)
|
|
|
|
|
- const oversizedFiles = files.filter((file) => file.size > IMAGE_MAX_SIZE)
|
|
|
|
|
- const validFiles = files.filter((file) => file.size <= IMAGE_MAX_SIZE)
|
|
|
|
|
|
|
+ const files = await chooseImages(source === 'camera' ? 1 : Math.min(remainingCount, 9), source)
|
|
|
|
|
+ // 拍照原图先缩放并加水印,再检查最终上传文件;iOS 原图可能超过 2MB。
|
|
|
|
|
+ const oversizedFiles = files.filter((file) => source === 'album' && file.size > IMAGE_MAX_SIZE)
|
|
|
|
|
+ const validFiles = files.filter((file) => source === 'camera' || file.size <= IMAGE_MAX_SIZE)
|
|
|
|
|
|
|
|
if (oversizedFiles.length > 0) {
|
|
if (oversizedFiles.length > 0) {
|
|
|
showMessage(`有${oversizedFiles.length}张图片超过2MB,已跳过`, 2500)
|
|
showMessage(`有${oversizedFiles.length}张图片超过2MB,已跳过`, 2500)
|
|
@@ -440,6 +440,7 @@ const uploadBatch = async (files: SelectedFile[], source: UploadSource): Promise
|
|
|
const isFileUpload = source === 'file'
|
|
const isFileUpload = source === 'file'
|
|
|
let uploadedCount = 0
|
|
let uploadedCount = 0
|
|
|
let failedCount = 0
|
|
let failedCount = 0
|
|
|
|
|
+ let firstFailureMessage = ''
|
|
|
|
|
|
|
|
uploading.value = true
|
|
uploading.value = true
|
|
|
|
|
|
|
@@ -473,7 +474,7 @@ const uploadBatch = async (files: SelectedFile[], source: UploadSource): Promise
|
|
|
showMessage('文件不能超过50MB')
|
|
showMessage('文件不能超过50MB')
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
- } else if (file.size > IMAGE_MAX_SIZE) {
|
|
|
|
|
|
|
+ } else if (source === 'album' && file.size > IMAGE_MAX_SIZE) {
|
|
|
showMessage('图片不能超过2MB')
|
|
showMessage('图片不能超过2MB')
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
@@ -488,10 +489,22 @@ const uploadBatch = async (files: SelectedFile[], source: UploadSource): Promise
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- const uploadPath =
|
|
|
|
|
- source === 'camera'
|
|
|
|
|
- ? await withTimeout(addWatermark(file.path), WATERMARK_TIMEOUT_MS, '图片处理超时')
|
|
|
|
|
- : file.path
|
|
|
|
|
|
|
+ const uploadPath = source === 'camera' ? await addWatermark(file.path) : file.path
|
|
|
|
|
+ if (source === 'camera') {
|
|
|
|
|
+ await new Promise<void>((resolve, reject) => {
|
|
|
|
|
+ uni.getFileInfo({
|
|
|
|
|
+ filePath: uploadPath,
|
|
|
|
|
+ success: (result) => {
|
|
|
|
|
+ if (result.size > IMAGE_MAX_SIZE) {
|
|
|
|
|
+ reject(new Error('图片处理后仍超过2MB,请重新拍摄'))
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ resolve()
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: reject,
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
const originalFileName = isFileUpload ? name : ''
|
|
const originalFileName = isFileUpload ? name : ''
|
|
|
const uploadedUrl = await uploadFile(uploadPath, originalFileName, isFileUpload)
|
|
const uploadedUrl = await uploadFile(uploadPath, originalFileName, isFileUpload)
|
|
|
|
|
|
|
@@ -499,6 +512,7 @@ const uploadBatch = async (files: SelectedFile[], source: UploadSource): Promise
|
|
|
uploadedCount += 1
|
|
uploadedCount += 1
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
failedCount += 1
|
|
failedCount += 1
|
|
|
|
|
+ firstFailureMessage ||= getErrorMessage(error)
|
|
|
|
|
|
|
|
if (pendingId) {
|
|
if (pendingId) {
|
|
|
removePendingAttachment(pendingId)
|
|
removePendingAttachment(pendingId)
|
|
@@ -525,7 +539,7 @@ const uploadBatch = async (files: SelectedFile[], source: UploadSource): Promise
|
|
|
? '上传成功'
|
|
? '上传成功'
|
|
|
: uploadedCount > 0
|
|
: uploadedCount > 0
|
|
|
? `${uploadedCount}张成功,${failedCount}张失败`
|
|
? `${uploadedCount}张成功,${failedCount}张失败`
|
|
|
- : '上传失败'
|
|
|
|
|
|
|
+ : firstFailureMessage || '上传失败'
|
|
|
|
|
|
|
|
uni.showToast({
|
|
uni.showToast({
|
|
|
title,
|
|
title,
|
|
@@ -834,107 +848,65 @@ const previewFile = (attachment: Attachment): void => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const addWatermark = (filePath: string): Promise<string> => {
|
|
const addWatermark = (filePath: string): Promise<string> => {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
|
- uni.getImageInfo({
|
|
|
|
|
- src: filePath,
|
|
|
|
|
- success: async (imageInfo) => {
|
|
|
|
|
- const sourceWidth = Number(imageInfo.width)
|
|
|
|
|
- const sourceHeight = Number(imageInfo.height)
|
|
|
|
|
-
|
|
|
|
|
- if (!sourceWidth || !sourceHeight) {
|
|
|
|
|
- reject(new Error('无法读取图片尺寸'))
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const canvasScale = Math.min(
|
|
|
|
|
- 1,
|
|
|
|
|
- MAX_WATERMARK_LONG_EDGE / Math.max(sourceWidth, sourceHeight)
|
|
|
|
|
- )
|
|
|
|
|
- const width = Math.max(1, Math.round(sourceWidth * canvasScale))
|
|
|
|
|
- const height = Math.max(1, Math.round(sourceHeight * canvasScale))
|
|
|
|
|
-
|
|
|
|
|
- watermarkCanvasWidth.value = width
|
|
|
|
|
- watermarkCanvasHeight.value = height
|
|
|
|
|
- await nextTick()
|
|
|
|
|
-
|
|
|
|
|
- const componentInstance = instance?.proxy
|
|
|
|
|
- const context = uni.createCanvasContext(
|
|
|
|
|
- canvasId.value,
|
|
|
|
|
- componentInstance
|
|
|
|
|
- ) as UniApp.CanvasContext & {
|
|
|
|
|
- measureText: (text: string) => { width: number }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const scaleRatio = width / 375
|
|
|
|
|
- const radius = 10 * scaleRatio
|
|
|
|
|
- const padding = 8 * scaleRatio
|
|
|
|
|
- const iconSize = 10 * scaleRatio
|
|
|
|
|
- const textSpacing = 4 * scaleRatio
|
|
|
|
|
- const lineHeight = 14 * scaleRatio
|
|
|
|
|
- const bottomSpacing = 14 * scaleRatio
|
|
|
|
|
- const leftSpacing = 14 * scaleRatio
|
|
|
|
|
- const textTop = 6 * scaleRatio
|
|
|
|
|
- const fontSize = 10 * scaleRatio
|
|
|
|
|
-
|
|
|
|
|
- context.drawImage(imageInfo.path || filePath, 0, 0, width, height)
|
|
|
|
|
- context.setFontSize(fontSize)
|
|
|
|
|
-
|
|
|
|
|
- const textRows = wrapText(
|
|
|
|
|
- context,
|
|
|
|
|
- watermarkAddress.value,
|
|
|
|
|
- width - iconSize - textSpacing - padding * 3 - leftSpacing * 2
|
|
|
|
|
- )
|
|
|
|
|
- textRows.push(formatDate(new Date()))
|
|
|
|
|
-
|
|
|
|
|
- const boxHeight = lineHeight * textRows.length + textTop
|
|
|
|
|
- const textWidth = Math.max(...textRows.map((text) => context.measureText(text).width), 0)
|
|
|
|
|
- const boxWidth = Math.min(
|
|
|
|
|
- width - leftSpacing * 2,
|
|
|
|
|
- Math.max(iconSize + textWidth + textSpacing * 3 + padding, padding * 6)
|
|
|
|
|
- )
|
|
|
|
|
- const boxX = leftSpacing
|
|
|
|
|
- const boxY = Math.max(height - boxHeight - bottomSpacing, bottomSpacing)
|
|
|
|
|
-
|
|
|
|
|
- drawRoundRect(context, boxX, boxY, boxWidth, boxHeight, radius)
|
|
|
|
|
- context.drawImage(
|
|
|
|
|
- '/static/images/task/watermarkIcon.png',
|
|
|
|
|
- boxX + padding,
|
|
|
|
|
- boxY + padding - 12,
|
|
|
|
|
- iconSize,
|
|
|
|
|
- iconSize
|
|
|
|
|
|
|
+ return renderTaskWatermark({
|
|
|
|
|
+ filePath,
|
|
|
|
|
+ canvasId: canvasId.value,
|
|
|
|
|
+ componentInstance: instance?.proxy,
|
|
|
|
|
+ resize: async (width, height) => {
|
|
|
|
|
+ watermarkCanvasWidth.value = width
|
|
|
|
|
+ watermarkCanvasHeight.value = height
|
|
|
|
|
+ await nextTick()
|
|
|
|
|
+ },
|
|
|
|
|
+ draw: (context, width, height, imagePath) => {
|
|
|
|
|
+ const scaleRatio = width / 375
|
|
|
|
|
+ const radius = 10 * scaleRatio
|
|
|
|
|
+ const padding = 8 * scaleRatio
|
|
|
|
|
+ const iconSize = 10 * scaleRatio
|
|
|
|
|
+ const textSpacing = 4 * scaleRatio
|
|
|
|
|
+ const lineHeight = 14 * scaleRatio
|
|
|
|
|
+ const bottomSpacing = 14 * scaleRatio
|
|
|
|
|
+ const leftSpacing = 14 * scaleRatio
|
|
|
|
|
+ const textTop = 6 * scaleRatio
|
|
|
|
|
+ const fontSize = 10 * scaleRatio
|
|
|
|
|
+
|
|
|
|
|
+ context.drawImage(imagePath, 0, 0, width, height)
|
|
|
|
|
+ context.setFontSize(fontSize)
|
|
|
|
|
+
|
|
|
|
|
+ const textRows = wrapText(
|
|
|
|
|
+ context,
|
|
|
|
|
+ watermarkAddress.value,
|
|
|
|
|
+ width - iconSize - textSpacing - padding * 3 - leftSpacing * 2
|
|
|
|
|
+ )
|
|
|
|
|
+ textRows.push(formatDate(new Date()))
|
|
|
|
|
+
|
|
|
|
|
+ const boxHeight = lineHeight * textRows.length + textTop
|
|
|
|
|
+ const textWidth = Math.max(...textRows.map((text) => context.measureText(text).width), 0)
|
|
|
|
|
+ const boxWidth = Math.min(
|
|
|
|
|
+ width - leftSpacing * 2,
|
|
|
|
|
+ Math.max(iconSize + textWidth + textSpacing * 3 + padding, padding * 6)
|
|
|
|
|
+ )
|
|
|
|
|
+ const boxX = leftSpacing
|
|
|
|
|
+ const boxY = Math.max(height - boxHeight - bottomSpacing, bottomSpacing)
|
|
|
|
|
+
|
|
|
|
|
+ drawRoundRect(context, boxX, boxY, boxWidth, boxHeight, radius)
|
|
|
|
|
+ context.drawImage(
|
|
|
|
|
+ '/static/images/task/watermarkIcon.png',
|
|
|
|
|
+ boxX + padding,
|
|
|
|
|
+ boxY + padding - 12,
|
|
|
|
|
+ iconSize,
|
|
|
|
|
+ iconSize
|
|
|
|
|
+ )
|
|
|
|
|
+ context.setFillStyle('#fff')
|
|
|
|
|
+ context.setFontSize(fontSize)
|
|
|
|
|
+
|
|
|
|
|
+ textRows.forEach((row, index) => {
|
|
|
|
|
+ context.fillText(
|
|
|
|
|
+ row,
|
|
|
|
|
+ boxX + iconSize + textSpacing + padding,
|
|
|
|
|
+ boxY + lineHeight * (index + 1)
|
|
|
)
|
|
)
|
|
|
- context.setFillStyle('#fff')
|
|
|
|
|
- context.setFontSize(fontSize)
|
|
|
|
|
-
|
|
|
|
|
- textRows.forEach((row, index) => {
|
|
|
|
|
- context.fillText(
|
|
|
|
|
- row,
|
|
|
|
|
- boxX + iconSize + textSpacing + padding,
|
|
|
|
|
- boxY + lineHeight * (index + 1)
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- context.draw(false, () => {
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- uni.canvasToTempFilePath(
|
|
|
|
|
- {
|
|
|
|
|
- canvasId: canvasId.value,
|
|
|
|
|
- fileType: 'jpg',
|
|
|
|
|
- width,
|
|
|
|
|
- height,
|
|
|
|
|
- destWidth: width,
|
|
|
|
|
- destHeight: height,
|
|
|
|
|
- quality: WATERMARK_JPEG_QUALITY,
|
|
|
|
|
- success: (result) => resolve(result.tempFilePath),
|
|
|
|
|
- fail: reject,
|
|
|
|
|
- },
|
|
|
|
|
- componentInstance
|
|
|
|
|
- )
|
|
|
|
|
- }, 300)
|
|
|
|
|
- })
|
|
|
|
|
- },
|
|
|
|
|
- fail: reject,
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1026,23 +998,6 @@ const isCancelError = (error: unknown): boolean => {
|
|
|
return getErrorMessage(error).toLowerCase().includes('cancel')
|
|
return getErrorMessage(error).toLowerCase().includes('cancel')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const withTimeout = <T,>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> => {
|
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
|
- const timer = setTimeout(() => reject(new Error(message)), timeoutMs)
|
|
|
|
|
-
|
|
|
|
|
- promise.then(
|
|
|
|
|
- (result) => {
|
|
|
|
|
- clearTimeout(timer)
|
|
|
|
|
- resolve(result)
|
|
|
|
|
- },
|
|
|
|
|
- (error) => {
|
|
|
|
|
- clearTimeout(timer)
|
|
|
|
|
- reject(error)
|
|
|
|
|
- }
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
const showMessage = (title: string, duration?: number): void => {
|
|
const showMessage = (title: string, duration?: number): void => {
|
|
|
uni.showToast({
|
|
uni.showToast({
|
|
|
title,
|
|
title,
|